@forwardimpact/pathway 0.25.21 → 0.25.24

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 (50) hide show
  1. package/bin/fit-pathway.js +117 -325
  2. package/package.json +2 -2
  3. package/src/commands/agent-io.js +1 -1
  4. package/src/commands/agent.js +25 -23
  5. package/src/commands/behaviour.js +7 -7
  6. package/src/commands/build-bundle.js +88 -0
  7. package/src/commands/build-packs.js +566 -0
  8. package/src/commands/build.js +27 -84
  9. package/src/commands/command-factory.js +2 -2
  10. package/src/commands/discipline.js +7 -7
  11. package/src/commands/driver.js +8 -8
  12. package/src/commands/index.js +0 -1
  13. package/src/commands/interview.js +4 -4
  14. package/src/commands/job.js +24 -18
  15. package/src/commands/level.js +7 -7
  16. package/src/commands/progress.js +4 -4
  17. package/src/commands/questions.js +10 -8
  18. package/src/commands/skill.js +10 -10
  19. package/src/commands/stage.js +7 -7
  20. package/src/commands/tool.js +6 -6
  21. package/src/commands/track.js +7 -7
  22. package/src/css/pages/agent-builder.css +48 -0
  23. package/src/formatters/interview/shared.js +6 -4
  24. package/src/formatters/progress/shared.js +9 -20
  25. package/src/formatters/questions/yaml.js +1 -1
  26. package/src/formatters/skill/shared.js +9 -2
  27. package/src/formatters/track/shared.js +4 -1
  28. package/src/index.html +1 -1
  29. package/src/lib/cli-command.js +33 -33
  30. package/src/lib/cli-output.js +9 -189
  31. package/src/pages/agent-builder-install.js +118 -0
  32. package/src/pages/agent-builder-preview.js +3 -3
  33. package/src/pages/agent-builder.js +23 -1
  34. package/src/pages/progress.js +3 -3
  35. package/src/pages/skill.js +5 -2
  36. package/src/commands/init.js +0 -64
  37. package/src/lib/job-cache.js +0 -89
  38. package/starter/behaviours/systems_thinking.yaml +0 -32
  39. package/starter/capabilities/delivery.yaml +0 -105
  40. package/starter/capabilities/reliability.yaml +0 -72
  41. package/starter/disciplines/software_engineering.yaml +0 -46
  42. package/starter/drivers.yaml +0 -10
  43. package/starter/framework.yaml +0 -49
  44. package/starter/levels.yaml +0 -39
  45. package/starter/questions/behaviours/.gitkeep +0 -0
  46. package/starter/questions/capabilities/.gitkeep +0 -0
  47. package/starter/questions/skills/.gitkeep +0 -0
  48. package/starter/stages.yaml +0 -21
  49. package/starter/tracks/forward_deployed.yaml +0 -33
  50. package/starter/tracks/platform.yaml +0 -33
@@ -95,11 +95,18 @@ export function prepareSkillDetail(
95
95
  if (!skill) return null;
96
96
 
97
97
  const relatedDisciplines = disciplines
98
- .filter((d) => getSkillTypeForDiscipline(d, skill.id) !== null)
98
+ .filter(
99
+ (d) =>
100
+ getSkillTypeForDiscipline({ discipline: d, skillId: skill.id }) !==
101
+ null,
102
+ )
99
103
  .map((d) => ({
100
104
  id: d.id,
101
105
  name: d.specialization || d.name,
102
- skillType: getSkillTypeForDiscipline(d, skill.id),
106
+ skillType: getSkillTypeForDiscipline({
107
+ discipline: d,
108
+ skillId: skill.id,
109
+ }),
103
110
  }));
104
111
 
105
112
  const relatedTracks = tracks
@@ -88,7 +88,10 @@ export function prepareTrackDetail(track, { skills, behaviours }) {
88
88
  const skillModifiers = track.skillModifiers
89
89
  ? Object.entries(track.skillModifiers).map(([key, modifier]) => {
90
90
  if (isCapability(key)) {
91
- const capabilitySkills = getSkillsByCapability(skills, key);
91
+ const capabilitySkills = getSkillsByCapability({
92
+ skills,
93
+ capability: key,
94
+ });
92
95
  return {
93
96
  id: key,
94
97
  name: key.charAt(0).toUpperCase() + key.slice(1),
package/src/index.html CHANGED
@@ -83,7 +83,7 @@
83
83
  <div class="top-bar__command">
84
84
  <span class="top-bar__prompt">$</span>
85
85
  <span class="top-bar__command-text" id="cli-command"
86
- >bunx fit-pathway</span
86
+ >npx fit-pathway</span
87
87
  >
88
88
  <button class="top-bar__copy" id="cli-copy" aria-label="Copy command">
89
89
  <svg viewBox="0 0 24 24">
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI Command Mapping
3
3
  *
4
- * Maps hash routes to their equivalent `bunx fit-pathway` CLI commands.
4
+ * Maps hash routes to their equivalent `npx fit-pathway` CLI commands.
5
5
  */
6
6
 
7
7
  /**
@@ -11,122 +11,122 @@
11
11
  */
12
12
  const ROUTE_COMMANDS = [
13
13
  // Landing
14
- { pattern: /^\/$/, toCommand: () => "bunx fit-pathway" },
14
+ { pattern: /^\/$/, toCommand: () => "npx fit-pathway" },
15
15
 
16
16
  // Entity lists
17
- { pattern: /^\/skill$/, toCommand: () => "bunx fit-pathway skill" },
18
- { pattern: /^\/behaviour$/, toCommand: () => "bunx fit-pathway behaviour" },
17
+ { pattern: /^\/skill$/, toCommand: () => "npx fit-pathway skill" },
18
+ { pattern: /^\/behaviour$/, toCommand: () => "npx fit-pathway behaviour" },
19
19
  {
20
20
  pattern: /^\/discipline$/,
21
- toCommand: () => "bunx fit-pathway discipline",
21
+ toCommand: () => "npx fit-pathway discipline",
22
22
  },
23
- { pattern: /^\/track$/, toCommand: () => "bunx fit-pathway track" },
24
- { pattern: /^\/level$/, toCommand: () => "bunx fit-pathway level" },
25
- { pattern: /^\/driver$/, toCommand: () => "bunx fit-pathway driver" },
26
- { pattern: /^\/stage$/, toCommand: () => "bunx fit-pathway stage" },
27
- { pattern: /^\/tool$/, toCommand: () => "bunx fit-pathway tool" },
23
+ { pattern: /^\/track$/, toCommand: () => "npx fit-pathway track" },
24
+ { pattern: /^\/level$/, toCommand: () => "npx fit-pathway level" },
25
+ { pattern: /^\/driver$/, toCommand: () => "npx fit-pathway driver" },
26
+ { pattern: /^\/stage$/, toCommand: () => "npx fit-pathway stage" },
27
+ { pattern: /^\/tool$/, toCommand: () => "npx fit-pathway tool" },
28
28
 
29
29
  // Entity details
30
30
  {
31
31
  pattern: /^\/skill\/(.+)$/,
32
- toCommand: (m) => `bunx fit-pathway skill ${m[1]}`,
32
+ toCommand: (m) => `npx fit-pathway skill ${m[1]}`,
33
33
  },
34
34
  {
35
35
  pattern: /^\/behaviour\/(.+)$/,
36
- toCommand: (m) => `bunx fit-pathway behaviour ${m[1]}`,
36
+ toCommand: (m) => `npx fit-pathway behaviour ${m[1]}`,
37
37
  },
38
38
  {
39
39
  pattern: /^\/discipline\/(.+)$/,
40
- toCommand: (m) => `bunx fit-pathway discipline ${m[1]}`,
40
+ toCommand: (m) => `npx fit-pathway discipline ${m[1]}`,
41
41
  },
42
42
  {
43
43
  pattern: /^\/track\/(.+)$/,
44
- toCommand: (m) => `bunx fit-pathway track ${m[1]}`,
44
+ toCommand: (m) => `npx fit-pathway track ${m[1]}`,
45
45
  },
46
46
  {
47
47
  pattern: /^\/level\/(.+)$/,
48
- toCommand: (m) => `bunx fit-pathway level ${m[1]}`,
48
+ toCommand: (m) => `npx fit-pathway level ${m[1]}`,
49
49
  },
50
50
  {
51
51
  pattern: /^\/driver\/(.+)$/,
52
- toCommand: (m) => `bunx fit-pathway driver ${m[1]}`,
52
+ toCommand: (m) => `npx fit-pathway driver ${m[1]}`,
53
53
  },
54
54
  {
55
55
  pattern: /^\/stage\/(.+)$/,
56
- toCommand: (m) => `bunx fit-pathway stage ${m[1]}`,
56
+ toCommand: (m) => `npx fit-pathway stage ${m[1]}`,
57
57
  },
58
58
 
59
59
  // Job builder + detail
60
60
  {
61
61
  pattern: /^\/job-builder$/,
62
- toCommand: () => "bunx fit-pathway job --list",
62
+ toCommand: () => "npx fit-pathway job --list",
63
63
  },
64
64
  {
65
65
  pattern: /^\/job\/([^/]+)\/([^/]+)\/([^/]+)$/,
66
- toCommand: (m) => `bunx fit-pathway job ${m[1]} ${m[2]} --track=${m[3]}`,
66
+ toCommand: (m) => `npx fit-pathway job ${m[1]} ${m[2]} --track=${m[3]}`,
67
67
  },
68
68
  {
69
69
  pattern: /^\/job\/([^/]+)\/([^/]+)$/,
70
- toCommand: (m) => `bunx fit-pathway job ${m[1]} ${m[2]}`,
70
+ toCommand: (m) => `npx fit-pathway job ${m[1]} ${m[2]}`,
71
71
  },
72
72
 
73
73
  // Interview builder + detail
74
74
  {
75
75
  pattern: /^\/interview-prep$/,
76
- toCommand: () => "bunx fit-pathway interview --list",
76
+ toCommand: () => "npx fit-pathway interview --list",
77
77
  },
78
78
  {
79
79
  pattern: /^\/interview\/([^/]+)\/([^/]+)\/([^/]+)$/,
80
80
  toCommand: (m) =>
81
- `bunx fit-pathway interview ${m[1]} ${m[2]} --track=${m[3]}`,
81
+ `npx fit-pathway interview ${m[1]} ${m[2]} --track=${m[3]}`,
82
82
  },
83
83
  {
84
84
  pattern: /^\/interview\/([^/]+)\/([^/]+)$/,
85
- toCommand: (m) => `bunx fit-pathway interview ${m[1]} ${m[2]}`,
85
+ toCommand: (m) => `npx fit-pathway interview ${m[1]} ${m[2]}`,
86
86
  },
87
87
 
88
88
  // Career progress builder + detail
89
89
  {
90
90
  pattern: /^\/career-progress$/,
91
- toCommand: () => "bunx fit-pathway progress --list",
91
+ toCommand: () => "npx fit-pathway progress --list",
92
92
  },
93
93
  {
94
94
  pattern: /^\/progress\/([^/]+)\/([^/]+)\/([^/]+)$/,
95
95
  toCommand: (m) =>
96
- `bunx fit-pathway progress ${m[1]} ${m[2]} --track=${m[3]}`,
96
+ `npx fit-pathway progress ${m[1]} ${m[2]} --track=${m[3]}`,
97
97
  },
98
98
  {
99
99
  pattern: /^\/progress\/([^/]+)\/([^/]+)$/,
100
- toCommand: (m) => `bunx fit-pathway progress ${m[1]} ${m[2]}`,
100
+ toCommand: (m) => `npx fit-pathway progress ${m[1]} ${m[2]}`,
101
101
  },
102
102
 
103
103
  // Self-assessment
104
104
  {
105
105
  pattern: /^\/self-assessment$/,
106
- toCommand: () => "bunx fit-pathway self-assessment",
106
+ toCommand: () => "npx fit-pathway self-assessment",
107
107
  },
108
108
  {
109
109
  pattern: /^\/self-assessment\/results$/,
110
- toCommand: () => "bunx fit-pathway self-assessment",
110
+ toCommand: () => "npx fit-pathway self-assessment",
111
111
  },
112
112
 
113
113
  // Agent builder + detail
114
114
  {
115
115
  pattern: /^\/agent-builder$/,
116
- toCommand: () => "bunx fit-pathway agent --list",
116
+ toCommand: () => "npx fit-pathway agent --list",
117
117
  },
118
118
  {
119
119
  pattern: /^\/agent\/([^/]+)\/([^/]+)\/([^/]+)$/,
120
120
  toCommand: (m) =>
121
- `bunx fit-pathway agent ${m[1]} --track=${m[2]} --stage=${m[3]}`,
121
+ `npx fit-pathway agent ${m[1]} --track=${m[2]} --stage=${m[3]}`,
122
122
  },
123
123
  {
124
124
  pattern: /^\/agent\/([^/]+)\/([^/]+)$/,
125
- toCommand: (m) => `bunx fit-pathway agent ${m[1]} --track=${m[2]}`,
125
+ toCommand: (m) => `npx fit-pathway agent ${m[1]} --track=${m[2]}`,
126
126
  },
127
127
  {
128
128
  pattern: /^\/agent\/([^/]+)$/,
129
- toCommand: (m) => `bunx fit-pathway agent ${m[1]}`,
129
+ toCommand: (m) => `npx fit-pathway agent ${m[1]}`,
130
130
  },
131
131
  ];
132
132
 
@@ -141,5 +141,5 @@ export function getCliCommand(hashPath) {
141
141
  const match = path.match(pattern);
142
142
  if (match) return toCommand(match);
143
143
  }
144
- return "bunx fit-pathway";
144
+ return "npx fit-pathway";
145
145
  }
@@ -1,129 +1,12 @@
1
1
  /**
2
- * CLI Output Formatting Utilities
2
+ * Domain-specific CLI Output Formatters
3
3
  *
4
- * Provides consistent formatting for terminal output including colors,
5
- * tables, headers, and level formatting.
4
+ * Pathway-specific formatting for skill proficiencies, behaviour maturities,
5
+ * modifiers, percentages, and change indicators. Generic formatters (colors,
6
+ * tables, headers, etc.) live in @forwardimpact/libcli.
6
7
  */
7
8
 
8
- // ANSI color codes
9
- export const colors = {
10
- reset: "\x1b[0m",
11
- bold: "\x1b[1m",
12
- dim: "\x1b[2m",
13
- italic: "\x1b[3m",
14
- underline: "\x1b[4m",
15
- red: "\x1b[31m",
16
- green: "\x1b[32m",
17
- yellow: "\x1b[33m",
18
- blue: "\x1b[34m",
19
- magenta: "\x1b[35m",
20
- cyan: "\x1b[36m",
21
- white: "\x1b[37m",
22
- gray: "\x1b[90m",
23
- };
24
-
25
- /**
26
- * Check if stdout supports colors
27
- * @returns {boolean}
28
- */
29
- export function supportsColor() {
30
- if (process.env.NO_COLOR) return false;
31
- if (process.env.FORCE_COLOR) return true;
32
- return process.stdout.isTTY;
33
- }
34
-
35
- /**
36
- * Wrap text with color if supported
37
- * @param {string} text
38
- * @param {string} color
39
- * @returns {string}
40
- */
41
- function colorize(text, color) {
42
- if (!supportsColor()) return text;
43
- return `${color}${text}${colors.reset}`;
44
- }
45
-
46
- /**
47
- * Format a header
48
- * @param {string} text
49
- * @returns {string}
50
- */
51
- export function formatHeader(text) {
52
- return colorize(text, colors.bold + colors.cyan);
53
- }
54
-
55
- /**
56
- * Format a subheader
57
- * @param {string} text
58
- * @returns {string}
59
- */
60
- export function formatSubheader(text) {
61
- return colorize(text, colors.bold);
62
- }
63
-
64
- /**
65
- * Format a list item
66
- * @param {string} label
67
- * @param {string} value
68
- * @param {number} [indent=0]
69
- * @returns {string}
70
- */
71
- export function formatListItem(label, value, indent = 0) {
72
- const padding = " ".repeat(indent);
73
- const bullet = colorize("•", colors.dim);
74
- return `${padding}${bullet} ${label}: ${value}`;
75
- }
76
-
77
- /**
78
- * Format a bullet item (no label)
79
- * @param {string} text
80
- * @param {number} [indent=0]
81
- * @returns {string}
82
- */
83
- export function formatBullet(text, indent = 0) {
84
- const padding = " ".repeat(indent);
85
- const bullet = colorize("•", colors.dim);
86
- return `${padding}${bullet} ${text}`;
87
- }
88
-
89
- /**
90
- * Format a table
91
- * @param {string[]} headers
92
- * @param {Array<Array<string|number>>} rows
93
- * @param {Object} [options]
94
- * @param {boolean} [options.compact=false]
95
- * @returns {string}
96
- */
97
- export function formatTable(headers, rows, options = {}) {
98
- const { compact = false } = options;
99
-
100
- // Calculate column widths
101
- const widths = headers.map((h, i) =>
102
- Math.max(String(h).length, ...rows.map((r) => String(r[i] || "").length)),
103
- );
104
-
105
- const lines = [];
106
-
107
- // Header
108
- const headerLine = headers
109
- .map((h, i) => String(h).padEnd(widths[i]))
110
- .join(" ");
111
- lines.push(colorize(headerLine, colors.bold));
112
-
113
- // Separator
114
- if (!compact) {
115
- lines.push(widths.map((w) => "─".repeat(w)).join("──"));
116
- }
117
-
118
- // Rows
119
- for (const row of rows) {
120
- lines.push(
121
- row.map((cell, i) => String(cell || "").padEnd(widths[i])).join(" "),
122
- );
123
- }
124
-
125
- return lines.join("\n");
126
- }
9
+ import { colorize, colors } from "@forwardimpact/libcli";
127
10
 
128
11
  /**
129
12
  * Format skill proficiency with color
@@ -193,78 +76,15 @@ export function formatPercent(value) {
193
76
  }
194
77
 
195
78
  /**
196
- * Format a change indicator (↑, ↓, →)
79
+ * Format a change indicator
197
80
  * @param {number} change
198
81
  * @returns {string}
199
82
  */
200
83
  export function formatChange(change) {
201
84
  if (change > 0) {
202
- return colorize(`↑${change}`, colors.green);
85
+ return colorize(`\u2191${change}`, colors.green);
203
86
  } else if (change < 0) {
204
- return colorize(`↓${Math.abs(change)}`, colors.red);
87
+ return colorize(`\u2193${Math.abs(change)}`, colors.red);
205
88
  }
206
- return colorize("", colors.dim);
207
- }
208
-
209
- /**
210
- * Format an error message
211
- * @param {string} message
212
- * @returns {string}
213
- */
214
- export function formatError(message) {
215
- return colorize(`Error: ${message}`, colors.red);
216
- }
217
-
218
- /**
219
- * Format a success message
220
- * @param {string} message
221
- * @returns {string}
222
- */
223
- export function formatSuccess(message) {
224
- return colorize(message, colors.green);
225
- }
226
-
227
- /**
228
- * Format a warning message
229
- * @param {string} message
230
- /**
231
- * Format a warning message
232
- * @param {string} message
233
- * @returns {string}
234
- */
235
- export function formatWarning(message) {
236
- return colorize(`Warning: ${message}`, colors.yellow);
237
- }
238
-
239
- /**
240
- * Create a horizontal rule
241
- * @param {number} [width=60]
242
- * @returns {string}
243
- */
244
- export function horizontalRule(width = 60) {
245
- return colorize("─".repeat(width), colors.dim);
246
- }
247
-
248
- /**
249
- * Format a section with title and content
250
- * @param {string} title
251
- * @param {string} content
252
- * @returns {string}
253
- */
254
- export function formatSection(title, content) {
255
- return `${formatHeader(title)}\n\n${content}`;
256
- }
257
-
258
- /**
259
- * Indent all lines of text
260
- * @param {string} text
261
- * @param {number} [spaces=2]
262
- * @returns {string}
263
- */
264
- export function indent(text, spaces = 2) {
265
- const padding = " ".repeat(spaces);
266
- return text
267
- .split("\n")
268
- .map((line) => padding + line)
269
- .join("\n");
89
+ return colorize("\u2192", colors.dim);
270
90
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Agent builder install section
3
+ *
4
+ * Surfaces the ecosystem-tool install commands (Microsoft APM and
5
+ * `npx skills`) for the currently selected discipline/track pack. The packs
6
+ * themselves are emitted by `fit-pathway build` when
7
+ * `framework.distribution.siteUrl` is configured — see spec 320 and
8
+ * `products/pathway/src/commands/build-packs.js`. The pack name derivation
9
+ * here must stay in sync with that generator so the command points at an
10
+ * archive that actually exists on the deployed site.
11
+ */
12
+
13
+ import { code, div, h2, p, section } from "../lib/render.js";
14
+ import {
15
+ getDisciplineAbbreviation,
16
+ toKebabCase,
17
+ } from "@forwardimpact/libskill/agent";
18
+ import { createCommandPrompt } from "../components/command-prompt.js";
19
+
20
+ /** Stable id for the install section heading (for aria-labelledby). */
21
+ const INSTALL_HEADING_ID = "agent-builder-install-heading";
22
+
23
+ /**
24
+ * Derive the pack archive name for a discipline/track combination.
25
+ * Must match `build-packs.js` → `${abbrev}-${toKebabCase(track.id)}`.
26
+ * @param {{id: string}} discipline
27
+ * @param {{id: string}} track
28
+ * @returns {string}
29
+ */
30
+ export function getPackName(discipline, track) {
31
+ return `${getDisciplineAbbreviation(discipline.id)}-${toKebabCase(track.id)}`;
32
+ }
33
+
34
+ /**
35
+ * Normalize a site URL by stripping a trailing slash. Matches the
36
+ * normalization applied by `generatePacks` so the displayed URL lines up
37
+ * with the manifest entries.
38
+ * @param {string} siteUrl
39
+ * @returns {string}
40
+ */
41
+ function normalizeSiteUrl(siteUrl) {
42
+ return siteUrl.replace(/\/$/, "");
43
+ }
44
+
45
+ /**
46
+ * Build the `apm install` command for a specific pack archive. Targets the
47
+ * direct archive URL (rather than a registry-style shorthand) because it is
48
+ * the most durable path through APM's evolving resolution logic and matches
49
+ * the URL listed in the generated `apm.yml` manifest.
50
+ * @param {string} siteUrl
51
+ * @param {string} packName
52
+ * @returns {string}
53
+ */
54
+ export function getApmInstallCommand(siteUrl, packName) {
55
+ return `apm install ${normalizeSiteUrl(siteUrl)}/packs/${packName}.tar.gz`;
56
+ }
57
+
58
+ /**
59
+ * Build the `npx skills add` command that discovers the per-pack skill
60
+ * repository at `<siteUrl>/packs/<packName>/.well-known/skills/index.json`.
61
+ * @param {string} siteUrl
62
+ * @param {string} packName
63
+ * @returns {string}
64
+ */
65
+ export function getSkillsAddCommand(siteUrl, packName) {
66
+ return `npx skills add ${normalizeSiteUrl(siteUrl)}/packs/${packName}`;
67
+ }
68
+
69
+ /**
70
+ * Render the install section for the selected agent combination. Returns
71
+ * `null` when no site URL is configured (no packs have been published, so
72
+ * there is nothing meaningful to install) so the caller can skip rendering.
73
+ * @param {Object} params
74
+ * @param {{id: string}} params.discipline - Selected human discipline
75
+ * @param {{id: string}} params.track - Selected human track
76
+ * @param {string|undefined} params.siteUrl - Framework distribution site URL
77
+ * @returns {HTMLElement|null}
78
+ */
79
+ export function createInstallSection({ discipline, track, siteUrl }) {
80
+ if (!siteUrl) return null;
81
+
82
+ const packName = getPackName(discipline, track);
83
+ const apmCommand = getApmInstallCommand(siteUrl, packName);
84
+ const skillsCommand = getSkillsAddCommand(siteUrl, packName);
85
+
86
+ return section(
87
+ {
88
+ className: "agent-install-section",
89
+ "aria-labelledby": INSTALL_HEADING_ID,
90
+ },
91
+ div(
92
+ { className: "agent-install-header" },
93
+ h2({ id: INSTALL_HEADING_ID }, "📦 Install This Agent Team"),
94
+ p(
95
+ { className: "text-muted agent-install-description" },
96
+ "Install the pre-built pack for this discipline × track combination " +
97
+ "directly through an ecosystem package manager. The pack contains " +
98
+ "the same stage agents, skills, team instructions, and Claude Code " +
99
+ "settings shown below — installed into your project's ",
100
+ code({}, ".claude/"),
101
+ " directory.",
102
+ ),
103
+ ),
104
+ div(
105
+ { className: "agent-install-commands" },
106
+ div(
107
+ { className: "agent-install-command" },
108
+ p({ className: "agent-install-command-label" }, "Microsoft APM"),
109
+ createCommandPrompt(apmCommand),
110
+ ),
111
+ div(
112
+ { className: "agent-install-command" },
113
+ p({ className: "agent-install-command-label" }, "npx skills"),
114
+ createCommandPrompt(skillsCommand),
115
+ ),
116
+ ),
117
+ );
118
+ }
@@ -10,8 +10,8 @@ import {
10
10
  deriveStageAgent,
11
11
  generateSkillMarkdown,
12
12
  deriveAgentSkills,
13
- deriveToolkit,
14
- } from "@forwardimpact/libskill";
13
+ } from "@forwardimpact/libskill/agent";
14
+ import { deriveToolkit } from "@forwardimpact/libskill/toolkit";
15
15
  import { getStageEmoji } from "../formatters/stage/shared.js";
16
16
  import { formatAgentProfile } from "../formatters/agent/profile.js";
17
17
  import {
@@ -115,7 +115,7 @@ function deriveSkillData(context) {
115
115
  const skillFiles = derivedSkills
116
116
  .map((d) => skills.find((s) => s.id === d.skillId))
117
117
  .filter((skill) => skill?.agent)
118
- .map((skill) => generateSkillMarkdown(skill, stages));
118
+ .map((skill) => generateSkillMarkdown({ skillData: skill, stages }));
119
119
 
120
120
  const toolkit = deriveToolkit({
121
121
  skillMatrix: derivedSkills,
@@ -18,7 +18,7 @@ import {
18
18
  } from "../lib/render.js";
19
19
  import { getState } from "../lib/state.js";
20
20
  import { loadAgentDataBrowser } from "../lib/yaml-loader.js";
21
- import { deriveReferenceLevel } from "@forwardimpact/libskill";
21
+ import { deriveReferenceLevel } from "@forwardimpact/libskill/agent";
22
22
  import {
23
23
  createSelectWithValue,
24
24
  createDisciplineSelect,
@@ -30,6 +30,7 @@ import {
30
30
  createSingleStagePreview,
31
31
  createHelpSection,
32
32
  } from "./agent-builder-preview.js";
33
+ import { createInstallSection } from "./agent-builder-install.js";
33
34
 
34
35
  /** All stages option value */
35
36
  const ALL_STAGES_VALUE = "all";
@@ -79,6 +80,7 @@ async function getTemplates() {
79
80
  */
80
81
  export async function renderAgentBuilder() {
81
82
  const { data } = getState();
83
+ const siteUrl = data.framework?.distribution?.siteUrl;
82
84
 
83
85
  // Show loading state
84
86
  render(
@@ -260,8 +262,27 @@ export async function renderAgentBuilder() {
260
262
  templates,
261
263
  };
262
264
 
265
+ // Install section (ecosystem-tool install commands) — appears above the
266
+ // preview cards so the install action is visible before users scroll
267
+ // through the generated files. Only rendered when the framework has a
268
+ // published distribution site URL, since the packs only exist at that
269
+ // URL after a `fit-pathway build`. Must come after the stage-validity
270
+ // guard below so an invalid stage id (e.g. from a stale bookmark) does
271
+ // not pair the install card with a "Stage not found" error.
272
+ function appendInstallSection() {
273
+ const installSection = createInstallSection({
274
+ discipline: humanDiscipline,
275
+ track: humanTrack,
276
+ siteUrl,
277
+ });
278
+ if (installSection) {
279
+ previewContainer.appendChild(installSection);
280
+ }
281
+ }
282
+
263
283
  // Generate preview based on stage selection
264
284
  if (stage === ALL_STAGES_VALUE) {
285
+ appendInstallSection();
265
286
  previewContainer.appendChild(createAllStagesPreview(context));
266
287
  } else {
267
288
  const stageObj = stages.find((s) => s.id === stage);
@@ -274,6 +295,7 @@ export async function renderAgentBuilder() {
274
295
  );
275
296
  return;
276
297
  }
298
+ appendInstallSection();
277
299
  previewContainer.appendChild(createSingleStagePreview(context, stageObj));
278
300
  }
279
301
  }
@@ -16,8 +16,8 @@ import {
16
16
  prepareCurrentJob,
17
17
  prepareCustomProgression,
18
18
  getDefaultTargetLevel,
19
- isValidCombination,
20
19
  } from "../formatters/progress/shared.js";
20
+ import { isValidJobCombination } from "@forwardimpact/libskill/derivation";
21
21
  import { buildComparisonResult } from "./progress-comparison.js";
22
22
 
23
23
  /**
@@ -188,7 +188,7 @@ function createComparisonSelectorsSection({
188
188
  for (const level of data.levels) {
189
189
  // Check trackless combination
190
190
  if (
191
- isValidCombination({ discipline: selectedDisc, level, track: null })
191
+ isValidJobCombination({ discipline: selectedDisc, level, track: null })
192
192
  ) {
193
193
  if (!validLevels.find((g) => g.id === level.id)) {
194
194
  validLevels.push(level);
@@ -197,7 +197,7 @@ function createComparisonSelectorsSection({
197
197
  }
198
198
  // Check each track combination
199
199
  for (const track of data.tracks) {
200
- if (isValidCombination({ discipline: selectedDisc, level, track })) {
200
+ if (isValidJobCombination({ discipline: selectedDisc, level, track })) {
201
201
  if (!validLevels.find((g) => g.id === level.id)) {
202
202
  validLevels.push(level);
203
203
  }
@@ -11,7 +11,7 @@ import { prepareSkillsList } from "../formatters/skill/shared.js";
11
11
  import { skillToDOM } from "../formatters/skill/dom.js";
12
12
  import { skillToCardConfig } from "../lib/card-mappers.js";
13
13
  import { getCapabilityEmoji, getConceptEmoji } from "@forwardimpact/map/levels";
14
- import { generateSkillMarkdown } from "@forwardimpact/libskill";
14
+ import { generateSkillMarkdown } from "@forwardimpact/libskill/agent";
15
15
  import { formatAgentSkill } from "../formatters/agent/skill.js";
16
16
 
17
17
  /** @type {string|null} Cached skill template */
@@ -101,7 +101,10 @@ export async function renderSkillDetail(params) {
101
101
  let agentSkillContent;
102
102
  if (skill.agent) {
103
103
  const template = await getSkillTemplate();
104
- const skillData = generateSkillMarkdown(skill, data.stages);
104
+ const skillData = generateSkillMarkdown({
105
+ skillData: skill,
106
+ stages: data.stages,
107
+ });
105
108
  agentSkillContent = formatAgentSkill(skillData, template);
106
109
  }
107
110