@forwardimpact/pathway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. package/package.json +68 -0
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Agent DOM Formatter
3
+ *
4
+ * Formats agent deployment data into DOM elements for browser display.
5
+ * Includes copy and download functionality.
6
+ */
7
+
8
+ import {
9
+ div,
10
+ h2,
11
+ h3,
12
+ p,
13
+ span,
14
+ button,
15
+ section,
16
+ details,
17
+ summary,
18
+ } from "../../lib/render.js";
19
+ import { formatAgentProfile } from "./profile.js";
20
+ import { formatAgentSkill } from "./skill.js";
21
+ import { getStageEmoji } from "../stage/shared.js";
22
+
23
+ /**
24
+ * Convert agent deployment to DOM elements
25
+ * @param {Object} deployment - Generated deployment
26
+ * @param {Object} deployment.profile - Agent profile
27
+ * @param {Array} deployment.skills - Agent skills
28
+ * @param {Array} [deployment.roleAgents] - Role variant agents (plan, review)
29
+ * @param {Object} [deployment.vscodeSettings] - VS Code settings to include in download
30
+ * @returns {HTMLElement}
31
+ */
32
+ export function agentDeploymentToDOM({
33
+ profile,
34
+ skills,
35
+ roleAgents = [],
36
+ vscodeSettings = {},
37
+ }) {
38
+ const profileContent = formatAgentProfile(profile);
39
+ const agentName = profile.frontmatter.name;
40
+
41
+ return div(
42
+ { className: "agent-deployment" },
43
+
44
+ // Download all button
45
+ createDownloadButton(
46
+ profile,
47
+ skills,
48
+ roleAgents,
49
+ vscodeSettings,
50
+ agentName,
51
+ ),
52
+
53
+ // Profile section
54
+ section(
55
+ { className: "agent-section" },
56
+ div(
57
+ { className: "section-header" },
58
+ h2({}, "Agent Profile"),
59
+ createCopyButton(profileContent),
60
+ ),
61
+ p({ className: "filename" }, profile.filename),
62
+ createCodeBlock(profileContent),
63
+ ),
64
+
65
+ // Role Agents section
66
+ roleAgents.length > 0
67
+ ? section(
68
+ { className: "agent-section" },
69
+ h2({}, `Role Variants (${roleAgents.length})`),
70
+ p(
71
+ { className: "text-muted" },
72
+ "Specialized agents for specific workflow phases. These can be used as subagents or standalone.",
73
+ ),
74
+ div(
75
+ { className: "role-agents-list" },
76
+ ...roleAgents.map((agent) => createRoleAgentCard(agent)),
77
+ ),
78
+ )
79
+ : null,
80
+
81
+ // Skills section
82
+ section(
83
+ { className: "agent-section" },
84
+ h2({}, `Skills (${skills.length})`),
85
+ skills.length > 0
86
+ ? div(
87
+ { className: "skills-list" },
88
+ ...skills.map((skill) => createSkillCard(skill)),
89
+ )
90
+ : p(
91
+ { className: "text-muted" },
92
+ "No skills with agent sections found for this discipline.",
93
+ ),
94
+ ),
95
+
96
+ // CLI hint section
97
+ section(
98
+ { className: "agent-section cli-hint" },
99
+ h2({}, "CLI Alternative"),
100
+ p({}, "Generate this agent from the command line:"),
101
+ createCliCommand(agentName),
102
+ ),
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Create download all button
108
+ * @param {Object} profile - Agent profile
109
+ * @param {Array} skills - Agent skills
110
+ * @param {Array} roleAgents - Role variant agents
111
+ * @param {Object} vscodeSettings - VS Code settings to include
112
+ * @param {string} agentName - Agent name for zip filename
113
+ * @returns {HTMLElement}
114
+ */
115
+ function createDownloadButton(
116
+ profile,
117
+ skills,
118
+ roleAgents,
119
+ vscodeSettings,
120
+ agentName,
121
+ ) {
122
+ const btn = button(
123
+ { className: "btn btn-primary download-all-btn" },
124
+ "📦 Download All (.zip)",
125
+ );
126
+
127
+ btn.addEventListener("click", async () => {
128
+ btn.disabled = true;
129
+ btn.textContent = "Generating...";
130
+
131
+ try {
132
+ await downloadAllAsZip(
133
+ profile,
134
+ skills,
135
+ roleAgents,
136
+ vscodeSettings,
137
+ agentName,
138
+ );
139
+ } finally {
140
+ btn.disabled = false;
141
+ btn.textContent = "📦 Download All (.zip)";
142
+ }
143
+ });
144
+
145
+ return btn;
146
+ }
147
+
148
+ /**
149
+ * Create a copy button for content
150
+ * @param {string} content - Content to copy
151
+ * @returns {HTMLElement}
152
+ */
153
+ function createCopyButton(content) {
154
+ const btn = button({ className: "btn btn-sm copy-btn" }, "📋 Copy");
155
+
156
+ btn.addEventListener("click", async () => {
157
+ try {
158
+ await navigator.clipboard.writeText(content);
159
+ btn.textContent = "✓ Copied";
160
+ setTimeout(() => {
161
+ btn.textContent = "📋 Copy";
162
+ }, 2000);
163
+ } catch {
164
+ btn.textContent = "Failed";
165
+ setTimeout(() => {
166
+ btn.textContent = "📋 Copy";
167
+ }, 2000);
168
+ }
169
+ });
170
+
171
+ return btn;
172
+ }
173
+
174
+ /**
175
+ * Create a code block with content
176
+ * @param {string} content - Code content
177
+ * @returns {HTMLElement}
178
+ */
179
+ function createCodeBlock(content) {
180
+ const pre = document.createElement("pre");
181
+ pre.className = "code-block";
182
+
183
+ const code = document.createElement("code");
184
+ code.textContent = content;
185
+
186
+ pre.appendChild(code);
187
+ return pre;
188
+ }
189
+
190
+ /**
191
+ * Create a skill card with content and copy button
192
+ * @param {Object} skill - Skill with frontmatter and body
193
+ * @returns {HTMLElement}
194
+ */
195
+ function createSkillCard(skill) {
196
+ const content = formatAgentSkill(skill);
197
+ const filename = `${skill.dirname}/SKILL.md`;
198
+
199
+ return div(
200
+ { className: "skill-card" },
201
+ div(
202
+ { className: "skill-header" },
203
+ span({ className: "skill-filename" }, filename),
204
+ createCopyButton(content),
205
+ ),
206
+ createCodeBlock(content),
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Create a role agent card with collapsible content
212
+ * @param {Object} agent - Role agent with frontmatter, body, filename
213
+ * @returns {HTMLElement}
214
+ */
215
+ function createRoleAgentCard(agent) {
216
+ const content = formatAgentProfile(agent);
217
+ const roleName = agent.frontmatter.name.split("-").pop(); // Extract role suffix (plan, review)
218
+
219
+ return details(
220
+ { className: "role-agent-card" },
221
+ summary(
222
+ {},
223
+ div(
224
+ { className: "role-agent-header" },
225
+ span(
226
+ { className: "role-name" },
227
+ `${roleName.charAt(0).toUpperCase() + roleName.slice(1)} Agent`,
228
+ ),
229
+ span({ className: "role-filename" }, agent.filename),
230
+ ),
231
+ ),
232
+ div(
233
+ { className: "role-agent-content" },
234
+ p(
235
+ { className: "text-muted role-description" },
236
+ agent.frontmatter.description,
237
+ ),
238
+ div({ className: "role-agent-actions" }, createCopyButton(content)),
239
+ createCodeBlock(content),
240
+ ),
241
+ );
242
+ }
243
+
244
+ /**
245
+ * Create CLI command display
246
+ * @param {string} agentName - Agent name (kebab-case)
247
+ * @returns {HTMLElement}
248
+ */
249
+ function createCliCommand(agentName) {
250
+ // Convert kebab-case name to discipline and track
251
+ const parts = agentName.split("-");
252
+ const track = parts.pop();
253
+ const discipline = parts.join("_");
254
+
255
+ const command = `npx pathway agent ${discipline} ${track} --output=.github --all-roles`;
256
+
257
+ const container = div(
258
+ { className: "cli-command" },
259
+ createCodeBlock(command),
260
+ createCopyButton(command),
261
+ );
262
+
263
+ return container;
264
+ }
265
+
266
+ /**
267
+ * Download all agent files as a ZIP
268
+ * @param {Object} profile - Agent profile
269
+ * @param {Array} skills - Agent skills
270
+ * @param {Array} roleAgents - Role variant agents
271
+ * @param {Object} vscodeSettings - VS Code settings to include
272
+ * @param {string} agentName - Agent name for zip filename
273
+ */
274
+ async function downloadAllAsZip(
275
+ profile,
276
+ skills,
277
+ roleAgents,
278
+ vscodeSettings,
279
+ agentName,
280
+ ) {
281
+ // Dynamically import JSZip
282
+ const JSZip = await importJSZip();
283
+ const zip = new JSZip();
284
+
285
+ // Add main profile to .github/agents/ folder
286
+ const profileContent = formatAgentProfile(profile);
287
+ zip.file(`.github/agents/${profile.filename}`, profileContent);
288
+
289
+ // Add role agent profiles to .github/agents/ folder
290
+ for (const roleAgent of roleAgents) {
291
+ const roleContent = formatAgentProfile(roleAgent);
292
+ zip.file(`.github/agents/${roleAgent.filename}`, roleContent);
293
+ }
294
+
295
+ // Add skills to .claude/skills/ folder
296
+ for (const skill of skills) {
297
+ const skillContent = formatAgentSkill(skill);
298
+ zip.file(`.claude/skills/${skill.dirname}/SKILL.md`, skillContent);
299
+ }
300
+
301
+ // Add VS Code settings for multi-agent features
302
+ if (Object.keys(vscodeSettings).length > 0) {
303
+ zip.file(
304
+ ".vscode/settings.json",
305
+ JSON.stringify(vscodeSettings, null, 2) + "\n",
306
+ );
307
+ }
308
+
309
+ // Generate and download
310
+ const blob = await zip.generateAsync({ type: "blob" });
311
+ const url = URL.createObjectURL(blob);
312
+
313
+ const link = document.createElement("a");
314
+ link.href = url;
315
+ link.download = `${agentName}-agent.zip`;
316
+ document.body.appendChild(link);
317
+ link.click();
318
+ document.body.removeChild(link);
319
+
320
+ URL.revokeObjectURL(url);
321
+ }
322
+
323
+ /**
324
+ * Dynamically import JSZip from CDN
325
+ * @returns {Promise<typeof JSZip>}
326
+ */
327
+ async function importJSZip() {
328
+ // Use dynamic import from CDN
329
+ const module = await import("https://cdn.jsdelivr.net/npm/jszip@3.10.1/+esm");
330
+ return module.default;
331
+ }
332
+
333
+ /**
334
+ * Format a stage-specific agent preview
335
+ * @param {Object} stageAgent - Derived stage agent data
336
+ * @param {Object} profile - Generated profile with frontmatter and body
337
+ * @param {Object} options - Options
338
+ * @param {Array} [options.stages] - All stages for emoji lookup
339
+ * @param {Object} [options.vscodeSettings] - VS Code settings for download
340
+ * @returns {HTMLElement}
341
+ */
342
+ export function stageAgentToDOM(stageAgent, profile, options = {}) {
343
+ const { vscodeSettings = {}, stages = [] } = options;
344
+ const { stage, tools, handoffs, constraints, checklist, derivedSkills } =
345
+ stageAgent;
346
+ const stageEmoji = getStageEmoji(stages, stage.id);
347
+ const profileContent = formatAgentProfile(profile);
348
+
349
+ return div(
350
+ { className: "agent-deployment stage-agent-preview" },
351
+
352
+ // Stage info header
353
+ section(
354
+ { className: "agent-section stage-info" },
355
+ div(
356
+ { className: "stage-header" },
357
+ span({ className: "stage-emoji" }, stageEmoji),
358
+ h2({}, `${stage.name} Agent`),
359
+ ),
360
+ p({ className: "text-muted" }, stage.description),
361
+ ),
362
+
363
+ // Tools section
364
+ tools.length > 0
365
+ ? section(
366
+ { className: "agent-section" },
367
+ h3({}, "Tools"),
368
+ div(
369
+ { className: "tool-badges" },
370
+ ...tools.map((tool) =>
371
+ span({ className: "badge badge-tool" }, tool),
372
+ ),
373
+ ),
374
+ )
375
+ : null,
376
+
377
+ // Constraints section
378
+ constraints.length > 0
379
+ ? section(
380
+ { className: "agent-section" },
381
+ h3({}, "Constraints"),
382
+ div(
383
+ { className: "constraint-list" },
384
+ ...constraints.map((c) =>
385
+ div({ className: "constraint-item" }, `⚠️ ${c}`),
386
+ ),
387
+ ),
388
+ )
389
+ : null,
390
+
391
+ // Handoffs section
392
+ handoffs.length > 0
393
+ ? section(
394
+ { className: "agent-section" },
395
+ h3({}, "Handoffs"),
396
+ div(
397
+ { className: "handoff-buttons" },
398
+ ...handoffs.map((h) => {
399
+ const targetEmoji = getStageEmoji(stages, h.target);
400
+ return div(
401
+ { className: "handoff-button-preview" },
402
+ span({ className: "handoff-icon" }, targetEmoji),
403
+ span({ className: "handoff-label" }, h.label),
404
+ );
405
+ }),
406
+ ),
407
+ )
408
+ : null,
409
+
410
+ // Checklist section
411
+ checklist.length > 0
412
+ ? section(
413
+ { className: "agent-section" },
414
+ h3({}, "Handoff Checklist"),
415
+ p(
416
+ { className: "text-muted" },
417
+ "Items to verify before transitioning to the next stage.",
418
+ ),
419
+ createChecklistPreview(checklist),
420
+ )
421
+ : null,
422
+
423
+ // Skills summary
424
+ derivedSkills.length > 0
425
+ ? section(
426
+ { className: "agent-section" },
427
+ h3({}, `Derived Skills (${derivedSkills.length})`),
428
+ div(
429
+ { className: "skill-badges" },
430
+ ...derivedSkills
431
+ .slice(0, 8)
432
+ .map((skill) =>
433
+ span({ className: "badge badge-default" }, skill.name),
434
+ ),
435
+ derivedSkills.length > 8
436
+ ? span(
437
+ { className: "text-muted" },
438
+ ` +${derivedSkills.length - 8} more`,
439
+ )
440
+ : null,
441
+ ),
442
+ )
443
+ : null,
444
+
445
+ // Profile section
446
+ section(
447
+ { className: "agent-section" },
448
+ div(
449
+ { className: "section-header" },
450
+ h3({}, "Agent Profile"),
451
+ createCopyButton(profileContent),
452
+ ),
453
+ p({ className: "filename" }, profile.filename),
454
+ createCodeBlock(profileContent),
455
+ ),
456
+
457
+ // Download button
458
+ createStageAgentDownloadButton(profile, vscodeSettings),
459
+ );
460
+ }
461
+
462
+ /**
463
+ * Create checklist preview grouped by capability
464
+ * @param {Array} checklist - Checklist items with capability and items
465
+ * @returns {HTMLElement}
466
+ */
467
+ function createChecklistPreview(checklist) {
468
+ return div(
469
+ { className: "checklist-preview" },
470
+ ...checklist.map((group) =>
471
+ div(
472
+ { className: "checklist-group" },
473
+ div({ className: "checklist-capability" }, group.capability),
474
+ div(
475
+ { className: "checklist-items" },
476
+ ...group.items.map((item) =>
477
+ div(
478
+ { className: "checklist-item" },
479
+ span({ className: "checklist-checkbox" }, "☐"),
480
+ span({}, item),
481
+ ),
482
+ ),
483
+ ),
484
+ ),
485
+ ),
486
+ );
487
+ }
488
+
489
+ /**
490
+ * Create download button for stage agent
491
+ * @param {Object} profile - Agent profile
492
+ * @param {Object} vscodeSettings - VS Code settings
493
+ * @returns {HTMLElement}
494
+ */
495
+ function createStageAgentDownloadButton(profile, vscodeSettings) {
496
+ const btn = button(
497
+ { className: "btn btn-primary download-all-btn" },
498
+ "📥 Download Agent Profile",
499
+ );
500
+
501
+ btn.addEventListener("click", async () => {
502
+ btn.disabled = true;
503
+ btn.textContent = "Generating...";
504
+
505
+ try {
506
+ const JSZip = await importJSZip();
507
+ const zip = new JSZip();
508
+
509
+ // Add profile
510
+ const profileContent = formatAgentProfile(profile);
511
+ zip.file(`.github/agents/${profile.filename}`, profileContent);
512
+
513
+ // Add VS Code settings
514
+ if (Object.keys(vscodeSettings).length > 0) {
515
+ zip.file(
516
+ ".vscode/settings.json",
517
+ JSON.stringify(vscodeSettings, null, 2) + "\n",
518
+ );
519
+ }
520
+
521
+ // Generate and download
522
+ const blob = await zip.generateAsync({ type: "blob" });
523
+ const url = URL.createObjectURL(blob);
524
+
525
+ const link = document.createElement("a");
526
+ link.href = url;
527
+ link.download = `${profile.frontmatter.name}-agent.zip`;
528
+ document.body.appendChild(link);
529
+ link.click();
530
+ document.body.removeChild(link);
531
+
532
+ URL.revokeObjectURL(url);
533
+ } finally {
534
+ btn.disabled = false;
535
+ btn.textContent = "📥 Download Agent Profile";
536
+ }
537
+ });
538
+
539
+ return btn;
540
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Agent Profile Formatter
3
+ *
4
+ * Formats agent profile data into .agent.md file content
5
+ * following the GitHub Copilot Custom Agents specification.
6
+ */
7
+
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
+ }
57
+
58
+ /**
59
+ * Format agent profile as .agent.md file content
60
+ * @param {Object} profile - Profile with frontmatter and body
61
+ * @param {Object} profile.frontmatter - YAML frontmatter data
62
+ * @param {string} profile.frontmatter.name - Agent name
63
+ * @param {string} profile.frontmatter.description - Agent description
64
+ * @param {string[]} profile.frontmatter.tools - Available tools
65
+ * @param {boolean} profile.frontmatter.infer - Whether to auto-select
66
+ * @param {Array} [profile.frontmatter.handoffs] - Handoff definitions
67
+ * @param {string} profile.body - Markdown body content
68
+ * @returns {string} Complete .agent.md file content
69
+ */
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");
101
+ }
102
+
103
+ /**
104
+ * Format agent profile for CLI output (markdown)
105
+ * @param {Object} profile - Profile with frontmatter and body
106
+ * @returns {string} Markdown formatted for CLI display
107
+ */
108
+ export function formatAgentProfileForCli({ frontmatter, body }) {
109
+ const lines = [];
110
+
111
+ lines.push(`# Agent Profile: ${frontmatter.name}`);
112
+ lines.push("");
113
+ lines.push(`**Description:** ${frontmatter.description}`);
114
+ lines.push("");
115
+ lines.push(`**Tools:** ${frontmatter.tools.join(", ")}`);
116
+ lines.push(`**Infer:** ${frontmatter.infer}`);
117
+
118
+ if (frontmatter.handoffs && frontmatter.handoffs.length > 0) {
119
+ lines.push("");
120
+ lines.push("**Handoffs:**");
121
+ for (const handoff of frontmatter.handoffs) {
122
+ const target = handoff.agent ? ` → ${handoff.agent}` : " (self)";
123
+ lines.push(` - ${handoff.label}${target}`);
124
+ }
125
+ }
126
+
127
+ lines.push("");
128
+ lines.push("---");
129
+ lines.push("");
130
+ lines.push(body);
131
+
132
+ return lines.join("\n");
133
+ }