@forwardimpact/pathway 0.1.0 → 0.3.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 (140) hide show
  1. package/app/commands/agent.js +119 -31
  2. package/app/commands/command-factory.js +3 -3
  3. package/app/commands/interview.js +14 -7
  4. package/app/commands/job.js +52 -33
  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 +117 -30
  10. package/app/css/components/surfaces.css +16 -0
  11. package/app/formatters/agent/profile.js +30 -115
  12. package/app/formatters/agent/skill.js +23 -44
  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 +5 -4
  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 +70 -81
  25. package/app/formatters/job/dom.js +40 -113
  26. package/app/formatters/job/markdown.js +17 -13
  27. package/app/formatters/json-ld.js +242 -0
  28. package/app/formatters/microdata-shared.js +184 -0
  29. package/app/formatters/progress/shared.js +14 -11
  30. package/app/formatters/shared.js +7 -2
  31. package/app/formatters/skill/dom.js +3 -0
  32. package/app/formatters/skill/microdata.js +151 -0
  33. package/app/formatters/stage/dom.js +3 -18
  34. package/app/formatters/stage/microdata.js +110 -0
  35. package/app/formatters/stage/shared.js +0 -27
  36. package/app/formatters/track/dom.js +5 -30
  37. package/app/formatters/track/markdown.js +2 -25
  38. package/app/formatters/track/microdata.js +111 -0
  39. package/app/formatters/track/shared.js +6 -58
  40. package/app/handout-main.js +26 -12
  41. package/app/handout.html +7 -0
  42. package/app/index.html +11 -0
  43. package/app/lib/card-mappers.js +17 -12
  44. package/app/lib/form-controls.js +64 -1
  45. package/app/lib/job-cache.js +12 -9
  46. package/app/lib/render.js +8 -1
  47. package/app/lib/template-loader.js +75 -0
  48. package/app/lib/yaml-loader.js +25 -8
  49. package/app/main.js +8 -4
  50. package/app/model/agent.js +158 -130
  51. package/app/model/checklist.js +57 -91
  52. package/app/model/derivation.js +135 -68
  53. package/app/model/index-generator.js +1 -7
  54. package/app/model/job.js +19 -13
  55. package/app/model/levels.js +20 -12
  56. package/app/model/loader.js +41 -17
  57. package/app/model/matching.js +33 -3
  58. package/app/model/profile.js +38 -45
  59. package/app/model/schema-validation.js +438 -0
  60. package/app/model/validation.js +747 -68
  61. package/app/pages/agent-builder.js +125 -28
  62. package/app/pages/assessment-results.js +10 -4
  63. package/app/pages/discipline.js +36 -6
  64. package/app/pages/driver.js +9 -47
  65. package/app/pages/interview-builder.js +3 -1
  66. package/app/pages/interview.js +15 -4
  67. package/app/pages/job-builder.js +4 -1
  68. package/app/pages/job.js +43 -8
  69. package/app/pages/landing.js +10 -10
  70. package/app/pages/progress-builder.js +3 -1
  71. package/app/pages/progress.js +78 -26
  72. package/app/pages/self-assessment.js +3 -3
  73. package/app/pages/stage.js +3 -126
  74. package/app/slide-main.js +45 -17
  75. package/app/slides/index.js +3 -1
  76. package/app/slides/overview.js +40 -4
  77. package/app/slides/progress.js +4 -2
  78. package/app/slides.html +7 -0
  79. package/bin/pathway.js +28 -75
  80. package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
  81. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
  82. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
  83. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
  84. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
  85. package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
  86. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
  87. package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
  88. package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
  89. package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
  90. package/examples/agents/.vscode/settings.json +1 -1
  91. package/examples/behaviours/outcome_ownership.yaml +1 -2
  92. package/examples/behaviours/polymathic_knowledge.yaml +1 -2
  93. package/examples/behaviours/precise_communication.yaml +1 -2
  94. package/examples/behaviours/relentless_curiosity.yaml +1 -2
  95. package/examples/behaviours/systems_thinking.yaml +1 -2
  96. package/examples/capabilities/business.yaml +80 -142
  97. package/examples/capabilities/delivery.yaml +155 -219
  98. package/examples/capabilities/people.yaml +2 -34
  99. package/examples/capabilities/reliability.yaml +161 -80
  100. package/examples/capabilities/scale.yaml +234 -252
  101. package/examples/copilot-setup-steps.yaml +25 -0
  102. package/examples/devcontainer.yaml +21 -0
  103. package/examples/disciplines/_index.yaml +1 -0
  104. package/examples/disciplines/data_engineering.yaml +14 -12
  105. package/examples/disciplines/engineering_management.yaml +63 -0
  106. package/examples/disciplines/software_engineering.yaml +14 -12
  107. package/examples/drivers.yaml +1 -4
  108. package/examples/framework.yaml +1 -2
  109. package/examples/grades.yaml +14 -15
  110. package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
  111. package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
  112. package/examples/questions/behaviours/precise_communication.yaml +1 -2
  113. package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
  114. package/examples/questions/behaviours/systems_thinking.yaml +1 -2
  115. package/examples/questions/skills/architecture_design.yaml +1 -2
  116. package/examples/questions/skills/cloud_platforms.yaml +1 -2
  117. package/examples/questions/skills/code_quality.yaml +1 -2
  118. package/examples/questions/skills/data_modeling.yaml +1 -2
  119. package/examples/questions/skills/devops.yaml +1 -2
  120. package/examples/questions/skills/full_stack_development.yaml +1 -2
  121. package/examples/questions/skills/sre_practices.yaml +1 -2
  122. package/examples/questions/skills/stakeholder_management.yaml +1 -2
  123. package/examples/questions/skills/team_collaboration.yaml +1 -2
  124. package/examples/questions/skills/technical_writing.yaml +1 -2
  125. package/examples/self-assessments.yaml +1 -3
  126. package/examples/stages.yaml +101 -46
  127. package/examples/tracks/_index.yaml +0 -1
  128. package/examples/tracks/platform.yaml +8 -13
  129. package/examples/tracks/sre.yaml +8 -18
  130. package/examples/vscode-settings.yaml +2 -7
  131. package/package.json +9 -3
  132. package/templates/agent.template.md +65 -0
  133. package/templates/job.template.md +47 -0
  134. package/templates/skill.template.md +28 -0
  135. package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
  136. package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
  137. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
  138. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
  139. package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
  140. package/examples/tracks/manager.yaml +0 -53
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
 
@@ -2,7 +2,7 @@
2
2
  * Reusable form control components
3
3
  */
4
4
 
5
- import { select, option } from "./render.js";
5
+ import { select, option, optgroup } from "./render.js";
6
6
 
7
7
  /**
8
8
  * Create a select element with initial value and change handler
@@ -45,3 +45,66 @@ export function createSelectWithValue({
45
45
 
46
46
  return selectEl;
47
47
  }
48
+
49
+ /**
50
+ * Create a discipline select with optgroups for Professional and Management
51
+ * @param {Object} options - Configuration options
52
+ * @param {string} options.id - Element ID
53
+ * @param {Array} options.disciplines - Array of discipline objects
54
+ * @param {string} options.initialValue - Initial selected value
55
+ * @param {string} options.placeholder - Placeholder text for empty option
56
+ * @param {Function} options.onChange - Callback when selection changes
57
+ * @param {Function} [options.getDisplayName] - Optional function to get display name from item
58
+ * @returns {HTMLElement}
59
+ */
60
+ export function createDisciplineSelect({
61
+ id,
62
+ disciplines,
63
+ initialValue,
64
+ placeholder,
65
+ onChange,
66
+ getDisplayName,
67
+ }) {
68
+ const displayFn = getDisplayName || ((d) => d.specialization || d.name);
69
+
70
+ // Separate disciplines by type
71
+ const professional = disciplines.filter((d) => d.isProfessional);
72
+ const management = disciplines.filter((d) => d.isManagement);
73
+
74
+ // Sort each group alphabetically by display name
75
+ professional.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
76
+ management.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
77
+
78
+ // Build options for a group
79
+ const buildOptions = (items) =>
80
+ items.map((item) => {
81
+ const opt = option({ value: item.id }, displayFn(item));
82
+ if (item.id === initialValue) {
83
+ opt.selected = true;
84
+ }
85
+ return opt;
86
+ });
87
+
88
+ // Build optgroups - Professional first, then Management
89
+ const groups = [];
90
+ if (professional.length > 0) {
91
+ groups.push(
92
+ optgroup({ label: "Professional" }, ...buildOptions(professional)),
93
+ );
94
+ }
95
+ if (management.length > 0) {
96
+ groups.push(optgroup({ label: "Management" }, ...buildOptions(management)));
97
+ }
98
+
99
+ const selectEl = select(
100
+ { className: "form-select", id },
101
+ option({ value: "" }, placeholder),
102
+ ...groups,
103
+ );
104
+
105
+ selectEl.addEventListener("change", (e) => {
106
+ onChange(e.target.value);
107
+ });
108
+
109
+ return selectEl;
110
+ }
@@ -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
  /**
package/app/lib/render.js CHANGED
@@ -118,6 +118,8 @@ export const select = (attrs, ...children) =>
118
118
  createElement("select", attrs, ...children);
119
119
  export const option = (attrs, ...children) =>
120
120
  createElement("option", attrs, ...children);
121
+ export const optgroup = (attrs, ...children) =>
122
+ createElement("optgroup", attrs, ...children);
121
123
  export const label = (attrs, ...children) =>
122
124
  createElement("label", attrs, ...children);
123
125
  export const form = (attrs, ...children) =>
@@ -185,12 +187,17 @@ export function showError(message) {
185
187
 
186
188
  /**
187
189
  * Format a skill level or behaviour maturity for display
190
+ * Handles both snake_case and camelCase
188
191
  * @param {string} value - The level/maturity value
189
192
  * @returns {string}
190
193
  */
191
194
  export function formatLevel(value) {
192
195
  if (!value) return "";
193
- return value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
196
+ // Insert space before uppercase letters (for camelCase), then handle snake_case
197
+ return value
198
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
199
+ .replace(/_/g, " ")
200
+ .replace(/\b\w/g, (c) => c.toUpperCase());
194
201
  }
195
202
 
196
203
  /**
@@ -0,0 +1,75 @@
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
+ }
67
+
68
+ /**
69
+ * Load job description template
70
+ * @param {string} dataDir - Path to data directory
71
+ * @returns {Promise<string>} Job template content
72
+ */
73
+ export async function loadJobTemplate(dataDir) {
74
+ return loadTemplate("job.template.md", dataDir);
75
+ }
@@ -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
  /**