@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
@@ -8,43 +8,12 @@ import { isCapability, getSkillsByCapability } from "../../model/modifiers.js";
8
8
  import { truncate } from "../shared.js";
9
9
 
10
10
  /**
11
- * Sort tracks by type: professional tracks first, then management tracks.
12
- * Within each type, preserves original order.
11
+ * Sort tracks alphabetically by name.
13
12
  * @param {Array} tracks - Raw track entities
14
13
  * @returns {Array} Sorted tracks array
15
14
  */
16
- export function sortTracksByType(tracks) {
17
- return [...tracks].sort((a, b) => {
18
- const aFlags = getTrackTypeFlags(a);
19
- const bFlags = getTrackTypeFlags(b);
20
-
21
- // Professional tracks come first
22
- if (aFlags.isProfessional && !bFlags.isProfessional) return -1;
23
- if (!aFlags.isProfessional && bFlags.isProfessional) return 1;
24
-
25
- // Preserve original order within same type
26
- return 0;
27
- });
28
- }
29
-
30
- /**
31
- * Determine track type flags from track data.
32
- *
33
- * Logic: Only one flag needs to be explicitly set to true; the other defaults to false.
34
- * - If isManagement: true → management track (isProfessional = false)
35
- * - If isProfessional: true (or neither set) → professional track (isManagement = false)
36
- *
37
- * @param {Object} track
38
- * @param {boolean} [track.isProfessional] - Whether this is a professional/IC track
39
- * @param {boolean} [track.isManagement] - Whether this is a management track
40
- * @returns {{isProfessional: boolean, isManagement: boolean}}
41
- */
42
- export function getTrackTypeFlags(track) {
43
- // Management takes precedence if explicitly set to true
44
- const isManagement = track.isManagement === true;
45
- // Professional is true if management is not true (default behavior)
46
- const isProfessional = !isManagement && track.isProfessional !== false;
47
- return { isProfessional, isManagement };
15
+ export function sortTracksByName(tracks) {
16
+ return [...tracks].sort((a, b) => a.name.localeCompare(b.name));
48
17
  }
49
18
 
50
19
  /**
@@ -53,8 +22,6 @@ export function getTrackTypeFlags(track) {
53
22
  * @property {string} name
54
23
  * @property {string} description
55
24
  * @property {string} truncatedDescription
56
- * @property {boolean} isProfessional
57
- * @property {boolean} isManagement
58
25
  */
59
26
 
60
27
  /**
@@ -64,16 +31,13 @@ export function getTrackTypeFlags(track) {
64
31
  * @returns {{ items: TrackListItem[] }}
65
32
  */
66
33
  export function prepareTracksList(tracks, descriptionLimit = 120) {
67
- const sortedTracks = sortTracksByType(tracks);
34
+ const sortedTracks = sortTracksByName(tracks);
68
35
  const items = sortedTracks.map((track) => {
69
- const { isProfessional, isManagement } = getTrackTypeFlags(track);
70
36
  return {
71
37
  id: track.id,
72
38
  name: track.name,
73
39
  description: track.description,
74
40
  truncatedDescription: truncate(track.description, descriptionLimit),
75
- isProfessional,
76
- isManagement,
77
41
  };
78
42
  });
79
43
 
@@ -101,11 +65,8 @@ export function prepareTracksList(tracks, descriptionLimit = 120) {
101
65
  * @property {string} id
102
66
  * @property {string} name
103
67
  * @property {string} description
104
- * @property {boolean} isProfessional
105
- * @property {boolean} isManagement
106
68
  * @property {SkillModifierRow[]} skillModifiers
107
69
  * @property {BehaviourModifierRow[]} behaviourModifiers
108
- * @property {Array<{id: string, name: string}>} validDisciplines
109
70
  */
110
71
 
111
72
  /**
@@ -114,14 +75,12 @@ export function prepareTracksList(tracks, descriptionLimit = 120) {
114
75
  * @param {Object} context - Additional context
115
76
  * @param {Array} context.skills - All skills
116
77
  * @param {Array} context.behaviours - All behaviours
117
- * @param {Array} context.disciplines - All disciplines
78
+ * @param {Array} context.disciplines - All disciplines (unused but kept for API compatibility)
118
79
  * @returns {TrackDetailView|null}
119
80
  */
120
- export function prepareTrackDetail(track, { skills, behaviours, disciplines }) {
81
+ export function prepareTrackDetail(track, { skills, behaviours }) {
121
82
  if (!track) return null;
122
83
 
123
- const { isProfessional, isManagement } = getTrackTypeFlags(track);
124
-
125
84
  // Build skill modifiers
126
85
  const skillModifiers = track.skillModifiers
127
86
  ? Object.entries(track.skillModifiers).map(([key, modifier]) => {
@@ -160,22 +119,11 @@ export function prepareTrackDetail(track, { skills, behaviours, disciplines }) {
160
119
  )
161
120
  : [];
162
121
 
163
- // Get valid disciplines
164
- const validDisciplines = track.validDisciplines
165
- ? track.validDisciplines
166
- .map((id) => disciplines.find((d) => d.id === id))
167
- .filter(Boolean)
168
- .map((d) => ({ id: d.id, name: d.specialization || d.name }))
169
- : [];
170
-
171
122
  return {
172
123
  id: track.id,
173
124
  name: track.name,
174
125
  description: track.description,
175
- isProfessional,
176
- isManagement,
177
126
  skillModifiers,
178
127
  behaviourModifiers,
179
- validDisciplines,
180
128
  };
181
129
  }
@@ -36,7 +36,7 @@ import {
36
36
  gradeToDOM,
37
37
  trackToDOM,
38
38
  } from "./formatters/index.js";
39
- import { sortTracksByType } from "./formatters/track/shared.js";
39
+ import { sortTracksByName } from "./formatters/track/shared.js";
40
40
 
41
41
  /**
42
42
  * Create a chapter cover page
@@ -271,6 +271,17 @@ function renderBehaviourHandout(data) {
271
271
  renderHandout(content);
272
272
  }
273
273
 
274
+ /**
275
+ * Sort disciplines by type (professional first, then management)
276
+ * @param {Array} disciplines - Raw discipline entities
277
+ * @returns {Array} - Sorted disciplines
278
+ */
279
+ function sortDisciplinesByType(disciplines) {
280
+ const professional = disciplines.filter((d) => !d.isManagement);
281
+ const management = disciplines.filter((d) => d.isManagement);
282
+ return [...professional, ...management];
283
+ }
284
+
274
285
  /**
275
286
  * Render all job component slides (disciplines, grades, tracks)
276
287
  * @param {Object} data
@@ -278,7 +289,10 @@ function renderBehaviourHandout(data) {
278
289
  function renderJobHandout(data) {
279
290
  const { framework } = data;
280
291
 
281
- const disciplineSlides = data.disciplines.map((discipline) => {
292
+ // Sort disciplines by type: professional first, then management
293
+ const sortedDisciplines = sortDisciplinesByType(data.disciplines);
294
+
295
+ const disciplineSlides = sortedDisciplines.map((discipline) => {
282
296
  return disciplineToDOM(discipline, {
283
297
  skills: data.skills,
284
298
  behaviours: data.behaviours,
@@ -295,7 +309,7 @@ function renderJobHandout(data) {
295
309
  });
296
310
  });
297
311
 
298
- const trackSlides = sortTracksByType(data.tracks).map((track) => {
312
+ const trackSlides = sortTracksByName(data.tracks).map((track) => {
299
313
  return trackToDOM(track, {
300
314
  skills: data.skills,
301
315
  behaviours: data.behaviours,
@@ -314,21 +328,21 @@ function renderJobHandout(data) {
314
328
  }),
315
329
  ...disciplineSlides,
316
330
 
317
- // Tracks chapter
318
- createChapterCover({
319
- emoji: getConceptEmoji(framework, "track"),
320
- title: framework.entityDefinitions.track.title,
321
- description: framework.entityDefinitions.track.description,
322
- }),
323
- ...trackSlides,
324
-
325
- // Grades chapter
331
+ // Grades chapter (moved before Tracks)
326
332
  createChapterCover({
327
333
  emoji: getConceptEmoji(framework, "grade"),
328
334
  title: framework.entityDefinitions.grade.title,
329
335
  description: framework.entityDefinitions.grade.description,
330
336
  }),
331
337
  ...gradeSlides,
338
+
339
+ // Tracks chapter (moved after Grades)
340
+ createChapterCover({
341
+ emoji: getConceptEmoji(framework, "track"),
342
+ title: framework.entityDefinitions.track.title,
343
+ description: framework.entityDefinitions.track.description,
344
+ }),
345
+ ...trackSlides,
332
346
  );
333
347
 
334
348
  renderHandout(content);
package/app/index.html CHANGED
@@ -5,6 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway</title>
7
7
  <link rel="stylesheet" href="css/bundles/app.css" />
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "mustache": "https://esm.sh/mustache@4.2.0"
12
+ }
13
+ }
14
+ </script>
8
15
  </head>
9
16
  <body>
10
17
  <div id="app">
@@ -51,6 +58,10 @@
51
58
 
52
59
  <footer id="app-footer">
53
60
  <p>Engineering Pathway</p>
61
+ <nav class="footer-links">
62
+ <a href="slides.html">Slides</a>
63
+ <a href="handout.html">Handouts</a>
64
+ </nav>
54
65
  </footer>
55
66
  </div>
56
67
 
@@ -15,10 +15,18 @@ import { getCapabilityEmoji } from "../model/levels.js";
15
15
  * @returns {Object}
16
16
  */
17
17
  export function disciplineToCardConfig(discipline) {
18
+ const badges = [];
19
+ if (discipline.isProfessional) {
20
+ badges.push(createBadge("Professional", "secondary"));
21
+ }
22
+ if (discipline.isManagement) {
23
+ badges.push(createBadge("Management", "primary"));
24
+ }
18
25
  return {
19
26
  title: discipline.name,
20
27
  description: discipline.truncatedDescription,
21
28
  href: `/discipline/${discipline.id}`,
29
+ badges,
22
30
  meta: [
23
31
  createBadge(`${discipline.coreSkillsCount} core`, "primary"),
24
32
  createBadge(
@@ -118,19 +126,11 @@ export function gradeToCardConfig(grade) {
118
126
  * @returns {Object}
119
127
  */
120
128
  export function trackToCardConfig(track) {
121
- const badges = [];
122
- if (track.isProfessional) {
123
- badges.push(createBadge("Professional", "secondary"));
124
- }
125
- if (track.isManagement) {
126
- badges.push(createBadge("Management", "default"));
127
- }
128
-
129
129
  return {
130
130
  title: track.name,
131
131
  description: track.truncatedDescription,
132
132
  href: `/track/${track.id}`,
133
- meta: badges,
133
+ meta: [],
134
134
  };
135
135
  }
136
136
 
@@ -140,12 +140,17 @@ export function trackToCardConfig(track) {
140
140
  * @returns {Object}
141
141
  */
142
142
  export function jobToCardConfig(job) {
143
+ const href = job.track
144
+ ? `/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
145
+ : `/job/${job.discipline.id}/${job.grade.id}`;
143
146
  return {
144
147
  title: job.title,
145
- description: `${job.discipline.specialization || job.discipline.name} at ${job.grade.professionalTitle} level on ${job.track.name} track`,
146
- href: `/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
148
+ description: job.track
149
+ ? `${job.discipline.specialization || job.discipline.name} at ${job.grade.professionalTitle} level on ${job.track.name} track`
150
+ : `${job.discipline.specialization || job.discipline.name} at ${job.grade.professionalTitle} level`,
151
+ href,
147
152
  badges: [createBadge(job.grade.id, "default")],
148
- meta: [createBadge(job.track.name, "secondary")],
153
+ meta: job.track ? [createBadge(job.track.name, "secondary")] : [],
149
154
  };
150
155
  }
151
156
 
@@ -13,12 +13,15 @@ const cache = new Map();
13
13
  /**
14
14
  * Create a consistent cache key from job parameters
15
15
  * @param {string} disciplineId
16
- * @param {string} trackId
17
16
  * @param {string} gradeId
17
+ * @param {string} [trackId] - Optional track ID
18
18
  * @returns {string}
19
19
  */
20
- export function makeJobKey(disciplineId, trackId, gradeId) {
21
- return `${disciplineId}_${trackId}_${gradeId}`;
20
+ export function makeJobKey(disciplineId, gradeId, trackId = null) {
21
+ if (trackId) {
22
+ return `${disciplineId}_${gradeId}_${trackId}`;
23
+ }
24
+ return `${disciplineId}_${gradeId}`;
22
25
  }
23
26
 
24
27
  /**
@@ -26,7 +29,7 @@ export function makeJobKey(disciplineId, trackId, gradeId) {
26
29
  * @param {Object} params
27
30
  * @param {Object} params.discipline
28
31
  * @param {Object} params.grade
29
- * @param {Object} params.track
32
+ * @param {Object} [params.track] - Optional track
30
33
  * @param {Array} params.skills
31
34
  * @param {Array} params.behaviours
32
35
  * @param {Array} [params.capabilities]
@@ -35,12 +38,12 @@ export function makeJobKey(disciplineId, trackId, gradeId) {
35
38
  export function getOrCreateJob({
36
39
  discipline,
37
40
  grade,
38
- track,
41
+ track = null,
39
42
  skills,
40
43
  behaviours,
41
44
  capabilities,
42
45
  }) {
43
- const key = makeJobKey(discipline.id, track.id, grade.id);
46
+ const key = makeJobKey(discipline.id, grade.id, track?.id);
44
47
 
45
48
  if (!cache.has(key)) {
46
49
  const job = deriveJob({
@@ -70,11 +73,11 @@ export function clearJobCache() {
70
73
  /**
71
74
  * Invalidate a specific job from the cache
72
75
  * @param {string} disciplineId
73
- * @param {string} trackId
74
76
  * @param {string} gradeId
77
+ * @param {string} [trackId] - Optional track ID
75
78
  */
76
- export function invalidateJob(disciplineId, trackId, gradeId) {
77
- cache.delete(makeJobKey(disciplineId, trackId, gradeId));
79
+ export function invalidateJob(disciplineId, gradeId, trackId = null) {
80
+ cache.delete(makeJobKey(disciplineId, gradeId, trackId));
78
81
  }
79
82
 
80
83
  /**
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Template Loader
3
+ *
4
+ * Loads Mustache templates from the data directory with fallback to the
5
+ * top-level templates directory. This allows users to customize agent
6
+ * and skill templates by placing them in their data directory.
7
+ *
8
+ * Resolution order:
9
+ * 1. {dataDir}/templates/{name} (user customization)
10
+ * 2. {codebaseDir}/templates/{name} (fallback)
11
+ */
12
+
13
+ import { readFile } from "fs/promises";
14
+ import { join, dirname } from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { existsSync } from "fs";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const CODEBASE_TEMPLATES_DIR = join(__dirname, "..", "..", "templates");
20
+
21
+ /**
22
+ * Load a template file with fallback to codebase templates
23
+ * @param {string} templateName - Template filename (e.g., 'agent.template.md')
24
+ * @param {string} dataDir - Path to data directory
25
+ * @returns {Promise<string>} Template content
26
+ * @throws {Error} If template not found in either location
27
+ */
28
+ export async function loadTemplate(templateName, dataDir) {
29
+ // Build list of paths to try
30
+ const paths = [];
31
+ if (dataDir) {
32
+ paths.push(join(dataDir, "templates", templateName));
33
+ }
34
+ paths.push(join(CODEBASE_TEMPLATES_DIR, templateName));
35
+
36
+ // Try each path in order
37
+ for (const path of paths) {
38
+ if (existsSync(path)) {
39
+ return await readFile(path, "utf-8");
40
+ }
41
+ }
42
+
43
+ // Not found
44
+ throw new Error(
45
+ `Template '${templateName}' not found. Checked:\n` +
46
+ paths.map((p) => ` - ${p}`).join("\n"),
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Load agent profile template
52
+ * @param {string} dataDir - Path to data directory
53
+ * @returns {Promise<string>} Agent template content
54
+ */
55
+ export async function loadAgentTemplate(dataDir) {
56
+ return loadTemplate("agent.template.md", dataDir);
57
+ }
58
+
59
+ /**
60
+ * Load agent skill template
61
+ * @param {string} dataDir - Path to data directory
62
+ * @returns {Promise<string>} Skill template content
63
+ */
64
+ export async function loadSkillTemplate(dataDir) {
65
+ return loadTemplate("skill.template.md", dataDir);
66
+ }
@@ -103,6 +103,11 @@ async function loadDisciplinesFromDir(disciplinesDir) {
103
103
  const {
104
104
  specialization,
105
105
  roleTitle,
106
+ // Track constraints
107
+ isProfessional,
108
+ isManagement,
109
+ validTracks,
110
+ minGrade,
106
111
  // Shared content - now at root level
107
112
  description,
108
113
  // Structural properties (derivation inputs)
@@ -118,6 +123,11 @@ async function loadDisciplinesFromDir(disciplinesDir) {
118
123
  id,
119
124
  specialization,
120
125
  roleTitle,
126
+ // Track constraints
127
+ isProfessional,
128
+ isManagement,
129
+ validTracks,
130
+ minGrade,
121
131
  // Shared content at top level
122
132
  description,
123
133
  // Structural properties
@@ -152,12 +162,10 @@ async function loadTracksFromDir(tracksDir) {
152
162
  description,
153
163
  roleContext,
154
164
  // Structural properties (derivation inputs)
155
- isProfessional,
156
- isManagement,
157
165
  skillModifiers,
158
166
  behaviourModifiers,
159
167
  matchingWeights,
160
- validDisciplines,
168
+ minGrade,
161
169
  // Agent section (no human section anymore for tracks)
162
170
  agent,
163
171
  } = content;
@@ -168,12 +176,10 @@ async function loadTracksFromDir(tracksDir) {
168
176
  description,
169
177
  roleContext,
170
178
  // Structural properties
171
- isProfessional,
172
- isManagement,
173
179
  skillModifiers,
174
180
  behaviourModifiers,
175
181
  matchingWeights,
176
- validDisciplines,
182
+ minGrade,
177
183
  ...(agent && { agent }),
178
184
  };
179
185
  }),
@@ -301,14 +307,23 @@ export async function loadAllData(dataDir = "./data") {
301
307
  * Load agent-specific data for browser-based agent generation
302
308
  * Uses co-located files where agent sections are embedded in entity files
303
309
  * @param {string} [dataDir='./data'] - Path to data directory
304
- * @returns {Promise<Object>} Agent data including disciplines, tracks, behaviours, vscodeSettings
310
+ * @returns {Promise<Object>} Agent data including disciplines, tracks, behaviours, vscodeSettings, devcontainer, copilotSetupSteps
305
311
  */
306
312
  export async function loadAgentDataBrowser(dataDir = "./data") {
307
- const [disciplines, tracks, behaviours, vscodeSettings] = await Promise.all([
313
+ const [
314
+ disciplines,
315
+ tracks,
316
+ behaviours,
317
+ vscodeSettings,
318
+ devcontainer,
319
+ copilotSetupSteps,
320
+ ] = await Promise.all([
308
321
  loadDisciplinesFromDir(`${dataDir}/disciplines`),
309
322
  loadTracksFromDir(`${dataDir}/tracks`),
310
323
  loadBehavioursFromDir(`${dataDir}/behaviours`),
311
324
  tryLoadYamlFile(`${dataDir}/vscode-settings.yaml`),
325
+ tryLoadYamlFile(`${dataDir}/devcontainer.yaml`),
326
+ tryLoadYamlFile(`${dataDir}/copilot-setup-steps.yaml`),
312
327
  ]);
313
328
 
314
329
  // Extract agent sections from co-located files
@@ -323,5 +338,7 @@ export async function loadAgentDataBrowser(dataDir = "./data") {
323
338
  .filter((b) => b.agent)
324
339
  .map((b) => ({ id: b.id, ...b.agent })),
325
340
  vscodeSettings: vscodeSettings || {},
341
+ devcontainer: devcontainer || {},
342
+ copilotSetupSteps: copilotSetupSteps || null,
326
343
  };
327
344
  }
package/app/main.js CHANGED
@@ -101,15 +101,18 @@ function setupRoutes() {
101
101
 
102
102
  // Job builder
103
103
  router.on("/job-builder", renderJobBuilder);
104
- router.on("/job/:discipline/:track/:grade", renderJobDetail);
104
+ router.on("/job/:discipline/:grade/:track", renderJobDetail);
105
+ router.on("/job/:discipline/:grade", renderJobDetail);
105
106
 
106
107
  // Interview prep
107
108
  router.on("/interview-prep", renderInterviewPrep);
108
- router.on("/interview/:discipline/:track/:grade", renderInterviewDetail);
109
+ router.on("/interview/:discipline/:grade/:track", renderInterviewDetail);
110
+ router.on("/interview/:discipline/:grade", renderInterviewDetail);
109
111
 
110
112
  // Career progress
111
113
  router.on("/career-progress", renderCareerProgress);
112
- router.on("/progress/:discipline/:track/:grade", renderProgressDetail);
114
+ router.on("/progress/:discipline/:grade/:track", renderProgressDetail);
115
+ router.on("/progress/:discipline/:grade", renderProgressDetail);
113
116
 
114
117
  // Self-assessment
115
118
  router.on("/self-assessment", renderSelfAssessment);
@@ -117,8 +120,9 @@ function setupRoutes() {
117
120
 
118
121
  // Agent builder
119
122
  router.on("/agent-builder", renderAgentBuilder);
120
- router.on("/agent/:discipline/:track", renderAgentBuilder);
121
123
  router.on("/agent/:discipline/:track/:stage", renderAgentBuilder);
124
+ router.on("/agent/:discipline/:track", renderAgentBuilder);
125
+ router.on("/agent/:discipline", renderAgentBuilder);
122
126
  }
123
127
 
124
128
  /**