@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
@@ -18,8 +18,10 @@ 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";
22
+ import { createToolIcon } from "../../lib/card-mappers.js";
21
23
  import { SKILL_LEVEL_ORDER } from "../../model/levels.js";
22
- import { prepareSkillDetail, formatCapability } from "./shared.js";
24
+ import { prepareSkillDetail } from "./shared.js";
23
25
  import { createJsonLdScript, skillToJsonLd } from "../json-ld.js";
24
26
 
25
27
  /**
@@ -31,11 +33,19 @@ import { createJsonLdScript, skillToJsonLd } from "../json-ld.js";
31
33
  * @param {Array} context.drivers - All drivers
32
34
  * @param {Array} context.capabilities - Capability entities
33
35
  * @param {boolean} [context.showBackLink=true] - Whether to show back navigation link
36
+ * @param {boolean} [context.showToolsAndPatterns=true] - Whether to show recommended tools and implementation patterns
34
37
  * @returns {HTMLElement}
35
38
  */
36
39
  export function skillToDOM(
37
40
  skill,
38
- { disciplines, tracks, drivers, capabilities, showBackLink = true } = {},
41
+ {
42
+ disciplines,
43
+ tracks,
44
+ drivers,
45
+ capabilities,
46
+ showBackLink = true,
47
+ showToolsAndPatterns = true,
48
+ } = {},
39
49
  ) {
40
50
  const view = prepareSkillDetail(skill, {
41
51
  disciplines,
@@ -61,7 +71,7 @@ export function skillToDOM(
61
71
  { className: "page-meta" },
62
72
  span(
63
73
  { className: "badge badge-default" },
64
- formatCapability(view.capability),
74
+ `${view.capabilityEmoji} ${view.capability.toUpperCase()}`,
65
75
  ),
66
76
  view.isHumanOnly
67
77
  ? span(
@@ -167,5 +177,62 @@ export function skillToDOM(
167
177
  ),
168
178
  )
169
179
  : null,
180
+
181
+ // Recommended Tools
182
+ showToolsAndPatterns && view.toolReferences.length > 0
183
+ ? div(
184
+ { className: "detail-section" },
185
+ heading2({ className: "section-title" }, "Recommended Tools"),
186
+ table(
187
+ { className: "tools-table" },
188
+ thead({}, tr({}, th({}, "Tool"), th({}, "Use When"))),
189
+ tbody(
190
+ {},
191
+ ...view.toolReferences.map((tool) =>
192
+ tr(
193
+ {},
194
+ td(
195
+ { className: "tool-name-cell" },
196
+ tool.simpleIcon
197
+ ? createToolIcon(tool.simpleIcon, tool.name)
198
+ : null,
199
+ tool.url
200
+ ? a(
201
+ {
202
+ href: tool.url,
203
+ target: "_blank",
204
+ rel: "noopener noreferrer",
205
+ },
206
+ tool.name,
207
+ )
208
+ : tool.name,
209
+ ),
210
+ td({}, tool.useWhen),
211
+ ),
212
+ ),
213
+ ),
214
+ ),
215
+ showBackLink
216
+ ? p(
217
+ { className: "see-all-link" },
218
+ a({ href: "#/tool" }, "See all tools →"),
219
+ )
220
+ : null,
221
+ )
222
+ : null,
223
+
224
+ // Implementation Reference
225
+ showToolsAndPatterns && view.implementationReference
226
+ ? div(
227
+ { className: "detail-section" },
228
+ heading2({ className: "section-title" }, "Implementation Patterns"),
229
+ createCodeDisplay({
230
+ content: view.implementationReference,
231
+ description:
232
+ "Project-specific implementation guidance for this skill.",
233
+ minHeight: 450,
234
+ }),
235
+ )
236
+ : null,
170
237
  );
171
238
  }
@@ -105,5 +105,23 @@ export function skillToMarkdown(
105
105
  lines.push("");
106
106
  }
107
107
 
108
+ // Recommended tools
109
+ if (view.toolReferences.length > 0) {
110
+ lines.push("## Recommended Tools", "");
111
+ const toolRows = view.toolReferences.map((tool) => [
112
+ tool.url ? `[${tool.name}](${tool.url})` : tool.name,
113
+ tool.useWhen,
114
+ ]);
115
+ lines.push(tableToMarkdown(["Tool", "Use When"], toolRows));
116
+ lines.push("");
117
+ }
118
+
119
+ // Implementation reference
120
+ if (view.implementationReference) {
121
+ lines.push("## Implementation Patterns", "");
122
+ lines.push(view.implementationReference);
123
+ lines.push("");
124
+ }
125
+
108
126
  return lines.join("\n");
109
127
  }
@@ -13,12 +13,12 @@ import { truncate } from "../shared.js";
13
13
 
14
14
  /**
15
15
  * Format capability name for display
16
- * @param {string} capability
16
+ * @param {string} capabilityName - The capability name to display
17
17
  * @returns {string}
18
18
  */
19
- export function formatCapability(capability) {
20
- if (!capability) return "";
21
- return capability.charAt(0).toUpperCase() + capability.slice(1);
19
+ export function formatCapability(capabilityName) {
20
+ if (!capabilityName) return "";
21
+ return capabilityName;
22
22
  }
23
23
 
24
24
  /**
@@ -66,12 +66,15 @@ export function prepareSkillsList(
66
66
  * @property {string} name
67
67
  * @property {string} description
68
68
  * @property {string} capability
69
+ * @property {string} capabilityName
69
70
  * @property {boolean} isHumanOnly
70
71
  * @property {string} capabilityEmoji
71
72
  * @property {Object<string, string>} levelDescriptions
72
73
  * @property {Array<{id: string, name: string, skillType: string}>} relatedDisciplines
73
74
  * @property {Array<{id: string, name: string, modifier: number}>} relatedTracks
74
75
  * @property {Array<{id: string, name: string}>} relatedDrivers
76
+ * @property {Array<{name: string, url?: string, description: string, useWhen: string}>} toolReferences
77
+ * @property {string|null} implementationReference
75
78
  */
76
79
 
77
80
  /**
@@ -110,16 +113,23 @@ export function prepareSkillDetail(
110
113
  .filter((d) => d.contributingSkills?.includes(skill.id))
111
114
  .map((d) => ({ id: d.id, name: d.name }));
112
115
 
116
+ const capabilityEntity = capabilities.find((c) => c.id === skill.capability);
117
+
113
118
  return {
114
119
  id: skill.id,
115
120
  name: skill.name,
116
121
  description: skill.description,
117
122
  capability: skill.capability,
123
+ capabilityName: capabilityEntity?.name || skill.capability,
118
124
  isHumanOnly: skill.isHumanOnly || false,
119
125
  capabilityEmoji: getCapabilityEmoji(capabilities, skill.capability),
120
126
  levelDescriptions: skill.levelDescriptions,
121
127
  relatedDisciplines,
122
128
  relatedTracks,
123
129
  relatedDrivers,
130
+ toolReferences: (skill.toolReferences || [])
131
+ .slice()
132
+ .sort((a, b) => a.name.localeCompare(b.name)),
133
+ implementationReference: skill.implementationReference || null,
124
134
  };
125
135
  }
@@ -32,7 +32,7 @@ export function stageListToMicrodata(stages) {
32
32
  ? `→ ${stage.handoffs.map((h) => h.target).join(", ")}`
33
33
  : "";
34
34
  return `${openTag("article", { itemtype: "Stage", itemid: `#${stage.id}` })}
35
- ${prop("h2", "name", `${stage.emoji || ""} ${stage.name}`)}
35
+ ${prop("h2", "name", `${stage.emojiIcon} ${stage.name}`)}
36
36
  ${prop("p", "description", stage.truncatedDescription)}
37
37
  ${handoffText ? `<p>Handoffs: ${handoffText}</p>` : ""}
38
38
  </article>`;
@@ -100,7 +100,7 @@ ${prop("p", "prompt", h.prompt)}
100
100
  ${openTag("article", { itemtype: "Stage", itemid: `#${view.id}` })}
101
101
  ${prop("h1", "name", view.name)}
102
102
  ${metaTag("id", view.id)}
103
- ${stage.emoji ? metaTag("emoji", stage.emoji) : ""}
103
+ ${stage.emojiIcon ? metaTag("emojiIcon", stage.emojiIcon) : ""}
104
104
  ${prop("p", "description", view.description)}
105
105
  ${sections.join("\n")}
106
106
  </article>
@@ -10,7 +10,7 @@ import { truncate } from "../shared.js";
10
10
  * @typedef {Object} StageListItem
11
11
  * @property {string} id
12
12
  * @property {string} name
13
- * @property {string} emoji
13
+ * @property {string} emojiIcon
14
14
  * @property {string} description
15
15
  * @property {string} truncatedDescription
16
16
  * @property {Array<{target: string, label: string}>} handoffs
@@ -27,7 +27,7 @@ export function prepareStagesList(stages, descriptionLimit = 150) {
27
27
  return {
28
28
  id: stage.id,
29
29
  name: stage.name,
30
- emoji: stage.emoji,
30
+ emojiIcon: stage.emojiIcon,
31
31
  description: stage.description,
32
32
  truncatedDescription: truncate(stage.description, descriptionLimit),
33
33
  handoffs: (stage.handoffs || []).map((h) => ({
@@ -80,5 +80,5 @@ export function prepareStageDetail(stage) {
80
80
  */
81
81
  export function getStageEmoji(stages, stageId) {
82
82
  const stage = stages.find((s) => s.id === stageId);
83
- return stage?.emoji || "🔄";
83
+ return stage?.emojiIcon;
84
84
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Tool presentation helpers
3
+ *
4
+ * Shared utilities for formatting tool data across DOM and CLI outputs.
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} ToolUsage
9
+ * @property {string} skillId
10
+ * @property {string} skillName
11
+ * @property {string} capabilityId
12
+ * @property {string} useWhen
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} AggregatedTool
17
+ * @property {string} name
18
+ * @property {string} [url]
19
+ * @property {string} [simpleIcon]
20
+ * @property {string} description
21
+ * @property {ToolUsage[]} usages
22
+ */
23
+
24
+ /**
25
+ * Aggregate tools from all skills, deduplicating by name
26
+ * @param {Array} skills - All skills with toolReferences
27
+ * @returns {AggregatedTool[]}
28
+ */
29
+ export function aggregateTools(skills) {
30
+ const toolMap = new Map();
31
+
32
+ for (const skill of skills) {
33
+ if (!skill.toolReferences) continue;
34
+
35
+ for (const tool of skill.toolReferences) {
36
+ const usage = {
37
+ skillId: skill.id,
38
+ skillName: skill.name,
39
+ capabilityId: skill.capability,
40
+ useWhen: tool.useWhen,
41
+ };
42
+
43
+ const existing = toolMap.get(tool.name);
44
+ if (existing) {
45
+ existing.usages.push(usage);
46
+ // Prefer simpleIcon from first occurrence that has one
47
+ if (!existing.simpleIcon && tool.simpleIcon) {
48
+ existing.simpleIcon = tool.simpleIcon;
49
+ }
50
+ } else {
51
+ toolMap.set(tool.name, {
52
+ name: tool.name,
53
+ url: tool.url,
54
+ simpleIcon: tool.simpleIcon,
55
+ description: tool.description,
56
+ usages: [usage],
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ return Array.from(toolMap.values()).sort((a, b) =>
63
+ a.name.localeCompare(b.name),
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Prepare tools list view data
69
+ * @param {Array} skills - All skills
70
+ * @returns {{ tools: AggregatedTool[], totalCount: number }}
71
+ */
72
+ export function prepareToolsList(skills) {
73
+ const tools = aggregateTools(skills);
74
+ return {
75
+ tools,
76
+ totalCount: tools.length,
77
+ };
78
+ }
@@ -41,17 +41,17 @@ import { sortTracksByName } from "./formatters/track/shared.js";
41
41
  /**
42
42
  * Create a chapter cover page
43
43
  * @param {Object} params
44
- * @param {string} params.emoji - Chapter emoji
44
+ * @param {string} params.emojiIcon - Chapter emoji
45
45
  * @param {string} params.title - Chapter title
46
46
  * @param {string} params.description - Chapter description
47
47
  * @returns {HTMLElement}
48
48
  */
49
- function createChapterCover({ emoji, title, description }) {
49
+ function createChapterCover({ emojiIcon, title, description }) {
50
50
  return div(
51
51
  { className: "chapter-cover" },
52
52
  h1(
53
53
  { className: "chapter-title" },
54
- emoji,
54
+ emojiIcon,
55
55
  " ",
56
56
  span({ className: "gradient-text" }, title),
57
57
  ),
@@ -111,7 +111,7 @@ function renderIndex(data) {
111
111
  { className: "page-header" },
112
112
  heading1(
113
113
  { className: "page-title" },
114
- `${framework.emoji} ${framework.title} Handouts`,
114
+ `${framework.emojiIcon} ${framework.title} Handouts`,
115
115
  ),
116
116
  p(
117
117
  { className: "page-description" },
@@ -135,25 +135,25 @@ function renderIndex(data) {
135
135
  `${getConceptEmoji(framework, "job")} ${framework.entityDefinitions.job.title}`,
136
136
  ),
137
137
  " - ",
138
- `${data.disciplines.length} disciplines, ${data.tracks.length} tracks, ${data.grades.length} grades`,
138
+ `${data.disciplines.length} disciplines, ${data.grades.length} grades, ${data.tracks.length} tracks`,
139
139
  ),
140
140
  li(
141
141
  {},
142
142
  a(
143
- { href: "#/skill" },
144
- `${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
143
+ { href: "#/behaviour" },
144
+ `${getConceptEmoji(framework, "behaviour")} ${framework.entityDefinitions.behaviour.title}`,
145
145
  ),
146
146
  " - ",
147
- `${data.skills.length} skill definitions`,
147
+ `${data.behaviours.length} behaviour definitions`,
148
148
  ),
149
149
  li(
150
150
  {},
151
151
  a(
152
- { href: "#/behaviour" },
153
- `${getConceptEmoji(framework, "behaviour")} ${framework.entityDefinitions.behaviour.title}`,
152
+ { href: "#/skill" },
153
+ `${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
154
154
  ),
155
155
  " - ",
156
- `${data.behaviours.length} behaviour definitions`,
156
+ `${data.skills.length} skill definitions`,
157
157
  ),
158
158
  li(
159
159
  {},
@@ -190,7 +190,7 @@ function renderDriverHandout(data) {
190
190
  const content = div(
191
191
  {},
192
192
  createChapterCover({
193
- emoji: getConceptEmoji(framework, "driver"),
193
+ emojiIcon: getConceptEmoji(framework, "driver"),
194
194
  title: framework.entityDefinitions.driver.title,
195
195
  description: framework.entityDefinitions.driver.description,
196
196
  }),
@@ -227,13 +227,14 @@ function renderSkillHandout(data) {
227
227
  drivers: data.drivers,
228
228
  capabilities: data.capabilities,
229
229
  showBackLink: false,
230
+ showToolsAndPatterns: false,
230
231
  });
231
232
  });
232
233
 
233
234
  const content = div(
234
235
  {},
235
236
  createChapterCover({
236
- emoji: getConceptEmoji(framework, "skill"),
237
+ emojiIcon: getConceptEmoji(framework, "skill"),
237
238
  title: framework.entityDefinitions.skill.title,
238
239
  description: framework.entityDefinitions.skill.description,
239
240
  }),
@@ -261,7 +262,7 @@ function renderBehaviourHandout(data) {
261
262
  const content = div(
262
263
  {},
263
264
  createChapterCover({
264
- emoji: getConceptEmoji(framework, "behaviour"),
265
+ emojiIcon: getConceptEmoji(framework, "behaviour"),
265
266
  title: framework.entityDefinitions.behaviour.title,
266
267
  description: framework.entityDefinitions.behaviour.description,
267
268
  }),
@@ -322,7 +323,7 @@ function renderJobHandout(data) {
322
323
  {},
323
324
  // Disciplines chapter
324
325
  createChapterCover({
325
- emoji: getConceptEmoji(framework, "discipline"),
326
+ emojiIcon: getConceptEmoji(framework, "discipline"),
326
327
  title: framework.entityDefinitions.discipline.title,
327
328
  description: framework.entityDefinitions.discipline.description,
328
329
  }),
@@ -330,7 +331,7 @@ function renderJobHandout(data) {
330
331
 
331
332
  // Grades chapter (moved before Tracks)
332
333
  createChapterCover({
333
- emoji: getConceptEmoji(framework, "grade"),
334
+ emojiIcon: getConceptEmoji(framework, "grade"),
334
335
  title: framework.entityDefinitions.grade.title,
335
336
  description: framework.entityDefinitions.grade.description,
336
337
  }),
@@ -338,7 +339,7 @@ function renderJobHandout(data) {
338
339
 
339
340
  // Tracks chapter (moved after Grades)
340
341
  createChapterCover({
341
- emoji: getConceptEmoji(framework, "track"),
342
+ emojiIcon: getConceptEmoji(framework, "track"),
342
343
  title: framework.entityDefinitions.track.title,
343
344
  description: framework.entityDefinitions.track.description,
344
345
  }),
@@ -393,7 +394,7 @@ function populateBrandHeader(framework) {
393
394
  header.appendChild(
394
395
  a(
395
396
  { className: "brand-title", href: "#/" },
396
- `${framework.emoji} ${framework.title}`,
397
+ `${framework.emojiIcon} ${framework.title}`,
397
398
  ),
398
399
  );
399
400
  header.appendChild(span({ className: "brand-tag" }, framework.tag));
package/app/index.html CHANGED
@@ -4,7 +4,19 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link
14
+ rel="stylesheet"
15
+ href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"
16
+ />
7
17
  <link rel="stylesheet" href="css/bundles/app.css" />
18
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
19
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markdown.min.js"></script>
8
20
  <script type="importmap">
9
21
  {
10
22
  "imports": {
@@ -31,12 +43,13 @@
31
43
  </button>
32
44
  <ul class="nav-links" id="nav-links">
33
45
  <li><a href="#/discipline">Disciplines</a></li>
34
- <li><a href="#/track">Tracks</a></li>
35
46
  <li><a href="#/grade">Grades</a></li>
36
- <li><a href="#/skill">Skills</a></li>
47
+ <li><a href="#/track">Tracks</a></li>
37
48
  <li><a href="#/behaviour">Behaviours</a></li>
38
- <li><a href="#/stage">Stages</a></li>
49
+ <li><a href="#/skill">Skills</a></li>
39
50
  <li><a href="#/driver">Drivers</a></li>
51
+ <li><a href="#/stage">Stages</a></li>
52
+ <li><a href="#/tool">Tools</a></li>
40
53
  <li><a href="#/job-builder" class="nav-cta">Build a Job</a></li>
41
54
  <li><a href="#/agent-builder" class="nav-cta">Build an Agent</a></li>
42
55
  <li><a href="#/interview-prep" class="nav-cta">Interview Prep</a></li>
@@ -9,6 +9,23 @@ import { createBadge } from "../components/card.js";
9
9
  import { formatLevel } from "./render.js";
10
10
  import { getCapabilityEmoji } from "../model/levels.js";
11
11
 
12
+ /**
13
+ * Create an external link element styled as a badge
14
+ * @param {string} text - Link text
15
+ * @param {string} url - External URL
16
+ * @returns {HTMLElement}
17
+ */
18
+ function createExternalLink(text, url) {
19
+ const link = document.createElement("a");
20
+ link.href = url;
21
+ link.target = "_blank";
22
+ link.rel = "noopener noreferrer";
23
+ link.className = "badge badge-primary";
24
+ link.textContent = text;
25
+ link.addEventListener("click", (e) => e.stopPropagation()); // Don't trigger card click
26
+ return link;
27
+ }
28
+
12
29
  /**
13
30
  * Map discipline to card config
14
31
  * @param {Object} discipline
@@ -155,24 +172,81 @@ export function jobToCardConfig(job) {
155
172
  }
156
173
 
157
174
  /**
158
- * Format capability for display
159
- * @param {string} capability
175
+ * Map tool to card config
176
+ * @param {Object} tool - Aggregated tool with usages
177
+ * @param {Array} capabilities - Capability entities for emoji lookup
178
+ * @returns {Object}
179
+ */
180
+ export function toolToCardConfig(tool, capabilities) {
181
+ // Create skills list as card content
182
+ const skillsList = createSkillsList(tool.usages, capabilities);
183
+
184
+ // Create icon element if available
185
+ const icon = tool.simpleIcon
186
+ ? createToolIcon(tool.simpleIcon, tool.name)
187
+ : null;
188
+
189
+ return {
190
+ title: tool.name,
191
+ description: tool.description,
192
+ // Docs link in header badges (upper right)
193
+ badges: tool.url ? [createExternalLink("Docs →", tool.url)] : [],
194
+ content: skillsList,
195
+ icon,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Create a tool icon element using Simple Icons CDN
201
+ * @param {string} slug - Simple Icons slug (e.g., 'terraform', 'docker')
202
+ * @param {string} name - Tool name for alt text
203
+ * @returns {HTMLElement}
204
+ */
205
+ export function createToolIcon(slug, name) {
206
+ const img = document.createElement("img");
207
+ // Use black color for consistent monochrome appearance
208
+ img.src = `https://cdn.simpleicons.org/${slug}/000000`;
209
+ img.alt = `${name} icon`;
210
+ img.className = "tool-icon";
211
+ img.width = 28;
212
+ img.height = 28;
213
+ // Gracefully handle missing icons
214
+ img.onerror = () => {
215
+ img.style.display = "none";
216
+ };
217
+ return img;
218
+ }
219
+
220
+ /**
221
+ * Create an unordered list of skill links with capability emoji
222
+ * @param {Array} usages - Tool usage objects with skillId, skillName, capabilityId
223
+ * @param {Array} capabilities - Capability entities
224
+ * @returns {HTMLElement}
225
+ */
226
+ function createSkillsList(usages, capabilities) {
227
+ const ul = document.createElement("ul");
228
+ ul.className = "tool-skills-list";
229
+
230
+ for (const usage of usages) {
231
+ const emoji = getCapabilityEmoji(capabilities, usage.capabilityId);
232
+ const li = document.createElement("li");
233
+ const link = document.createElement("a");
234
+ link.href = `#/skill/${usage.skillId}`;
235
+ link.textContent = `${emoji} ${usage.skillName}`;
236
+ li.appendChild(link);
237
+ ul.appendChild(li);
238
+ }
239
+
240
+ return ul;
241
+ }
242
+
243
+ /**
244
+ * Format capability for badge display (short, tag-like)
245
+ * @param {string} capabilityId
160
246
  * @param {Array} capabilities
161
247
  * @returns {string}
162
248
  */
163
- function formatCapability(capability, capabilities) {
164
- const capabilityLabels = {
165
- delivery: "Delivery",
166
- scale: "Scale",
167
- reliability: "Reliability",
168
- data: "Data",
169
- ai: "AI",
170
- process: "Process",
171
- business: "Business",
172
- people: "People",
173
- documentation: "Documentation",
174
- };
175
- const label = capabilityLabels[capability] || formatLevel(capability);
176
- const emoji = getCapabilityEmoji(capabilities, capability);
177
- return `${emoji} ${label}`;
249
+ function formatCapability(capabilityId, capabilities) {
250
+ const emoji = getCapabilityEmoji(capabilities, capabilityId);
251
+ return `${emoji} ${capabilityId.toUpperCase()}`;
178
252
  }
package/app/lib/render.js CHANGED
@@ -111,6 +111,10 @@ export const th = (attrs, ...children) =>
111
111
  createElement("th", attrs, ...children);
112
112
  export const td = (attrs, ...children) =>
113
113
  createElement("td", attrs, ...children);
114
+ export const pre = (attrs, ...children) =>
115
+ createElement("pre", attrs, ...children);
116
+ export const code = (attrs, ...children) =>
117
+ createElement("code", attrs, ...children);
114
118
  export const button = (attrs, ...children) =>
115
119
  createElement("button", attrs, ...children);
116
120
  export const input = (attrs) => createElement("input", attrs);
@@ -70,7 +70,15 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
70
70
 
71
71
  if (capability.skills && Array.isArray(capability.skills)) {
72
72
  for (const skill of capability.skills) {
73
- const { id, name, isHumanOnly, human, agent } = skill;
73
+ const {
74
+ id,
75
+ name,
76
+ isHumanOnly,
77
+ human,
78
+ agent,
79
+ implementationReference,
80
+ toolReferences,
81
+ } = skill;
74
82
  allSkills.push({
75
83
  id,
76
84
  name,
@@ -80,6 +88,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
80
88
  // Include isHumanOnly flag for agent filtering (defaults to false)
81
89
  ...(isHumanOnly && { isHumanOnly }),
82
90
  ...(agent && { agent }),
91
+ // Include implementation reference and tool references (shared by human and agent)
92
+ ...(implementationReference && { implementationReference }),
93
+ ...(toolReferences && { toolReferences }),
83
94
  });
84
95
  }
85
96
  }
package/app/main.js CHANGED
@@ -26,6 +26,7 @@ import { renderTracksList, renderTrackDetail } from "./pages/track.js";
26
26
  import { renderGradesList, renderGradeDetail } from "./pages/grade.js";
27
27
  import { renderDriversList, renderDriverDetail } from "./pages/driver.js";
28
28
  import { renderStagesList, renderStageDetail } from "./pages/stage.js";
29
+ import { renderToolsList } from "./pages/tool.js";
29
30
  import { renderJobBuilder } from "./pages/job-builder.js";
30
31
  import { renderJobDetail } from "./pages/job.js";
31
32
  import { renderInterviewPrep } from "./pages/interview-builder.js";
@@ -99,6 +100,9 @@ function setupRoutes() {
99
100
  router.on("/stage", renderStagesList);
100
101
  router.on("/stage/:id", renderStageDetail);
101
102
 
103
+ // Tool
104
+ router.on("/tool", renderToolsList);
105
+
102
106
  // Job builder
103
107
  router.on("/job-builder", renderJobBuilder);
104
108
  router.on("/job/:discipline/:grade/:track", renderJobDetail);