@forwardimpact/pathway 0.2.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.
@@ -7,16 +7,18 @@
7
7
  * All agents are stage-specific. Use --stage for a single stage
8
8
  * or --all-stages (default) for all stages.
9
9
  *
10
+ * By default, outputs to console. Use --output to write files.
11
+ *
10
12
  * Usage:
11
- * npx pathway agent <discipline> [--track=<track>] [--output=PATH] [--preview]
13
+ * npx pathway agent <discipline> [--track=<track>]
12
14
  * npx pathway agent <discipline> --track=<track> --stage=plan
13
- * npx pathway agent <discipline> --track=<track> --all-stages
15
+ * npx pathway agent <discipline> --track=<track> --output=./agents
14
16
  * npx pathway agent --list
15
17
  *
16
18
  * Examples:
17
19
  * npx pathway agent software_engineering --track=platform
18
20
  * npx pathway agent software_engineering --track=platform --stage=plan
19
- * npx pathway agent software_engineering --track=platform --preview
21
+ * npx pathway agent software_engineering --track=platform --output=./agents
20
22
  */
21
23
 
22
24
  import { writeFile, mkdir, readFile } from "fs/promises";
@@ -32,10 +34,7 @@ import {
32
34
  deriveAgentSkills,
33
35
  generateSkillMd,
34
36
  } from "../model/agent.js";
35
- import {
36
- formatAgentProfile,
37
- formatAgentProfileForCli,
38
- } from "../formatters/agent/profile.js";
37
+ import { formatAgentProfile } from "../formatters/agent/profile.js";
39
38
  import { formatAgentSkill } from "../formatters/agent/skill.js";
40
39
  import { formatError, formatSuccess } from "../lib/cli-output.js";
41
40
  import {
@@ -410,14 +409,15 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
410
409
  process.exit(1);
411
410
  }
412
411
 
413
- // Preview or write
414
- if (options.preview) {
415
- console.log(formatAgentProfileForCli(profile));
412
+ // Load template
413
+ const agentTemplate = await loadAgentTemplate(dataDir);
414
+
415
+ // Output to console (default) or write to files (with --output)
416
+ if (!options.output) {
417
+ console.log(formatAgentProfile(profile, agentTemplate));
416
418
  return;
417
419
  }
418
420
 
419
- // Load templates only when writing files
420
- const agentTemplate = await loadAgentTemplate(dataDir);
421
421
  await writeProfile(profile, baseDir, agentTemplate);
422
422
  await generateVSCodeSettings(baseDir, agentData.vscodeSettings);
423
423
  await generateDevcontainer(
@@ -484,19 +484,19 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
484
484
  }
485
485
  }
486
486
 
487
- // Preview or write
488
- if (options.preview) {
487
+ // Load templates
488
+ const agentTemplate = await loadAgentTemplate(dataDir);
489
+ const skillTemplate = await loadSkillTemplate(dataDir);
490
+
491
+ // Output to console (default) or write to files (with --output)
492
+ if (!options.output) {
489
493
  for (const profile of profiles) {
490
- console.log(formatAgentProfileForCli(profile));
494
+ console.log(formatAgentProfile(profile, agentTemplate));
491
495
  console.log("\n---\n");
492
496
  }
493
497
  return;
494
498
  }
495
499
 
496
- // Load templates only when writing files
497
- const agentTemplate = await loadAgentTemplate(dataDir);
498
- const skillTemplate = await loadSkillTemplate(dataDir);
499
-
500
500
  for (const profile of profiles) {
501
501
  await writeProfile(profile, baseDir, agentTemplate);
502
502
  }
@@ -20,15 +20,17 @@ import {
20
20
  deriveChecklist,
21
21
  formatChecklistMarkdown,
22
22
  } from "../model/checklist.js";
23
+ import { loadJobTemplate } from "../lib/template-loader.js";
23
24
 
24
25
  /**
25
26
  * Format job output
26
27
  * @param {Object} view - Presenter view
27
28
  * @param {Object} _options - Command options
28
29
  * @param {Object} entities - Original entities
30
+ * @param {string} jobTemplate - Mustache template for job description
29
31
  */
30
- function formatJob(view, _options, entities) {
31
- console.log(jobToMarkdown(view, entities));
32
+ function formatJob(view, _options, entities, jobTemplate) {
33
+ console.log(jobToMarkdown(view, entities, jobTemplate));
32
34
  }
33
35
 
34
36
  /**
@@ -37,8 +39,9 @@ function formatJob(view, _options, entities) {
37
39
  * @param {Object} params.data - All loaded data
38
40
  * @param {string[]} params.args - Command arguments
39
41
  * @param {Object} params.options - Command options
42
+ * @param {string} params.dataDir - Path to data directory
40
43
  */
41
- export async function runJobCommand({ data, args, options }) {
44
+ export async function runJobCommand({ data, args, options, dataDir }) {
42
45
  const jobs = generateAllJobs({
43
46
  disciplines: data.disciplines,
44
47
  grades: data.grades,
@@ -167,5 +170,7 @@ export async function runJobCommand({ data, args, options }) {
167
170
  return;
168
171
  }
169
172
 
170
- formatJob(view, options, { discipline, grade, track });
173
+ // Load job template for description formatting
174
+ const jobTemplate = await loadJobTemplate(dataDir);
175
+ formatJob(view, options, { discipline, grade, track }, jobTemplate);
171
176
  }
@@ -17,7 +17,10 @@ import {
17
17
  } from "../lib/render.js";
18
18
  import { getState } from "../lib/state.js";
19
19
  import { createBadge } from "./card.js";
20
- import { createSelectWithValue } from "../lib/form-controls.js";
20
+ import {
21
+ createSelectWithValue,
22
+ createDisciplineSelect,
23
+ } from "../lib/form-controls.js";
21
24
  import { createReactive } from "../lib/reactive.js";
22
25
 
23
26
  /**
@@ -213,9 +216,9 @@ export function createBuilder({
213
216
  div(
214
217
  { className: "form-group" },
215
218
  label({ className: "form-label" }, labels.discipline || "Discipline"),
216
- createSelectWithValue({
219
+ createDisciplineSelect({
217
220
  id: "discipline-select",
218
- items: data.disciplines,
221
+ disciplines: data.disciplines,
219
222
  initialValue: selection.get().discipline,
220
223
  placeholder: "Select a discipline...",
221
224
  onChange: (value) => {
@@ -46,114 +46,3 @@ export function formatAgentProfile({ frontmatter, bodyData }, template) {
46
46
  };
47
47
  return Mustache.render(template, data);
48
48
  }
49
-
50
- /**
51
- * Format agent profile for CLI output (markdown)
52
- * @param {Object} profile - Profile with frontmatter and bodyData
53
- * @returns {string} Markdown formatted for CLI display
54
- */
55
- export function formatAgentProfileForCli({ frontmatter, bodyData }) {
56
- const lines = [];
57
-
58
- lines.push(`# Agent Profile: ${frontmatter.name}`);
59
- lines.push("");
60
- lines.push(`**Description:** ${frontmatter.description}`);
61
- lines.push("");
62
- lines.push(`**Infer:** ${frontmatter.infer}`);
63
-
64
- if (frontmatter.handoffs && frontmatter.handoffs.length > 0) {
65
- lines.push("");
66
- lines.push("**Handoffs:**");
67
- for (const handoff of frontmatter.handoffs) {
68
- const target = handoff.agent ? ` → ${handoff.agent}` : " (self)";
69
- lines.push(` - ${handoff.label}${target}`);
70
- }
71
- }
72
-
73
- lines.push("");
74
- lines.push("---");
75
- lines.push("");
76
-
77
- // Render structured body data
78
- lines.push(`# ${bodyData.title}`);
79
- lines.push("");
80
- lines.push(bodyData.stageDescription);
81
- lines.push("");
82
-
83
- lines.push("## Core Identity");
84
- lines.push("");
85
- lines.push(bodyData.identity);
86
- lines.push("");
87
-
88
- if (bodyData.priority) {
89
- lines.push(bodyData.priority);
90
- lines.push("");
91
- }
92
-
93
- if (bodyData.capabilities && bodyData.capabilities.length > 0) {
94
- lines.push("Your primary capabilities:");
95
- for (const cap of bodyData.capabilities) {
96
- lines.push(`- ${cap}`);
97
- }
98
- lines.push("");
99
- }
100
-
101
- if (bodyData.beforeMakingChanges && bodyData.beforeMakingChanges.length > 0) {
102
- lines.push("Before making changes:");
103
- for (const step of bodyData.beforeMakingChanges) {
104
- lines.push(`${step.index}. ${step.text}`);
105
- }
106
- lines.push("");
107
- }
108
-
109
- if (bodyData.delegation) {
110
- lines.push("## Delegation");
111
- lines.push("");
112
- lines.push(bodyData.delegation);
113
- lines.push("");
114
- }
115
-
116
- lines.push("## Operational Context");
117
- lines.push("");
118
- lines.push(bodyData.operationalContext);
119
- lines.push("");
120
-
121
- lines.push(bodyData.workingStyle);
122
-
123
- if (bodyData.beforeHandoff) {
124
- lines.push("## Before Handoff");
125
- lines.push("");
126
- lines.push(
127
- "Before offering a handoff, verify and summarize completion of these items:",
128
- );
129
- lines.push("");
130
- lines.push(bodyData.beforeHandoff);
131
- lines.push("");
132
- lines.push(
133
- "When verified, summarize what was accomplished then offer the handoff.",
134
- );
135
- lines.push("If items are incomplete, explain what remains.");
136
- lines.push("");
137
- }
138
-
139
- lines.push("## Return Format");
140
- lines.push("");
141
- lines.push("When completing work (for handoff or as a subagent), provide:");
142
- lines.push("");
143
- lines.push("1. **Work completed**: What was accomplished");
144
- lines.push(
145
- "2. **Checklist status**: Items verified from Before Handoff section",
146
- );
147
- lines.push("3. **Recommendation**: Ready for next stage, or needs more work");
148
- lines.push("");
149
-
150
- if (bodyData.constraints && bodyData.constraints.length > 0) {
151
- lines.push("## Constraints");
152
- lines.push("");
153
- for (const constraint of bodyData.constraints) {
154
- lines.push(`- ${constraint}`);
155
- }
156
- }
157
-
158
- return lines.join("\n");
159
- }
@@ -35,57 +35,3 @@ export function formatAgentSkill(
35
35
  };
36
36
  return Mustache.render(template, data);
37
37
  }
38
-
39
- /**
40
- * Format agent skill for CLI output (markdown)
41
- * @param {Object} skill - Skill with frontmatter, title, stages, reference
42
- * @returns {string} Markdown formatted for CLI display
43
- */
44
- export function formatAgentSkillForCli({
45
- frontmatter,
46
- title,
47
- stages,
48
- reference,
49
- }) {
50
- const lines = [];
51
-
52
- lines.push(`# ${title}`);
53
- lines.push("");
54
- lines.push(`**Name:** ${frontmatter.name}`);
55
- lines.push("");
56
- lines.push(`**Description:** ${frontmatter.description.trim()}`);
57
- lines.push("");
58
-
59
- if (stages && stages.length > 0) {
60
- lines.push("## Stage Guidance");
61
- lines.push("");
62
- for (const stage of stages) {
63
- lines.push(`### ${stage.stageName} Stage`);
64
- lines.push("");
65
- lines.push(`**Focus:** ${stage.focus.trim()}`);
66
- lines.push("");
67
- if (stage.activities && stage.activities.length > 0) {
68
- lines.push("**Activities:**");
69
- for (const item of stage.activities) {
70
- lines.push(`- ${item}`);
71
- }
72
- lines.push("");
73
- }
74
- if (stage.ready && stage.ready.length > 0) {
75
- lines.push(`**Ready for ${stage.nextStageName} when:**`);
76
- for (const item of stage.ready) {
77
- lines.push(`- [ ] ${item}`);
78
- }
79
- lines.push("");
80
- }
81
- }
82
- }
83
-
84
- if (reference) {
85
- lines.push("## Reference");
86
- lines.push("");
87
- lines.push(reference.trim());
88
- }
89
-
90
- return lines.join("\n");
91
- }
@@ -14,6 +14,7 @@ import {
14
14
  tr,
15
15
  th,
16
16
  td,
17
+ formatLevel,
17
18
  } from "../../lib/render.js";
18
19
  import { createBackLink } from "../../components/nav.js";
19
20
  import { createLevelDots } from "../../components/detail.js";
@@ -102,10 +103,7 @@ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
102
103
  ...Object.entries(view.expectations).map(([key, value]) =>
103
104
  div(
104
105
  { className: "list-item" },
105
- p(
106
- { className: "label" },
107
- key.charAt(0).toUpperCase() + key.slice(1),
108
- ),
106
+ p({ className: "label" }, formatLevel(key)),
109
107
  p({}, value),
110
108
  ),
111
109
  ),
@@ -3,41 +3,28 @@
3
3
  *
4
4
  * Formats job data into markdown job description content.
5
5
  * Parallels formatters/agent/profile.js in structure.
6
+ *
7
+ * Uses Mustache templates for flexible output formatting.
8
+ * Templates are loaded from data/ directory with fallback to templates/ directory.
6
9
  */
7
10
 
11
+ import Mustache from "mustache";
12
+
8
13
  import {
9
14
  SKILL_LEVEL_ORDER,
10
15
  BEHAVIOUR_MATURITY_ORDER,
11
16
  } from "../../model/levels.js";
12
17
 
13
18
  /**
14
- * Format job as a markdown job description
19
+ * Prepare job data for template rendering
15
20
  * @param {Object} params
16
21
  * @param {Object} params.job - The job definition
17
22
  * @param {Object} params.discipline - The discipline
18
23
  * @param {Object} params.grade - The grade
19
- * @param {Object} params.track - The track
20
- * @returns {string} Markdown formatted job description
24
+ * @param {Object} [params.track] - The track (optional)
25
+ * @returns {Object} Data object ready for Mustache template
21
26
  */
22
- export function formatJobDescription({ job, discipline, grade, track }) {
23
- const lines = [];
24
-
25
- // Title
26
- lines.push(`# ${job.title}`);
27
- lines.push("");
28
-
29
- // Meta information
30
- lines.push(`- **Level:** ${grade.id}`);
31
- lines.push(`- **Experience:** ${grade.typicalExperienceRange}`);
32
- if (track) {
33
- lines.push(`- **Track:** ${track.name}`);
34
- }
35
- lines.push("");
36
-
37
- // Role Summary
38
- lines.push("## ROLE SUMMARY");
39
- lines.push("");
40
-
27
+ function prepareJobDescriptionData({ job, discipline, grade, track }) {
41
28
  // Build role summary from discipline - use manager version if applicable
42
29
  const isManagement = discipline.isManagement === true;
43
30
  let roleSummary =
@@ -48,16 +35,9 @@ export function formatJobDescription({ job, discipline, grade, track }) {
48
35
  const { roleTitle, specialization } = discipline;
49
36
  roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
50
37
  roleSummary = roleSummary.replace(/\{specialization\}/g, specialization);
51
- lines.push(roleSummary);
52
- lines.push("");
53
-
54
- // Add track context
55
- if (track?.roleContext) {
56
- lines.push(track.roleContext);
57
- lines.push("");
58
- }
59
38
 
60
- // Add grade expectations as natural paragraphs
39
+ // Build expectations paragraph
40
+ let expectationsParagraph = "";
61
41
  if (job.expectations) {
62
42
  const exp = job.expectations;
63
43
  const expectationSentences = [];
@@ -89,45 +69,20 @@ export function formatJobDescription({ job, discipline, grade, track }) {
89
69
  }
90
70
 
91
71
  if (expectationSentences.length > 0) {
92
- lines.push(expectationSentences.join(" "));
93
- lines.push("");
72
+ expectationsParagraph = expectationSentences.join(" ");
94
73
  }
95
74
  }
96
75
 
97
- // Key Responsibilities
98
- lines.push("## ROLE RESPONSIBILITIES");
99
- lines.push("");
100
-
101
- // Use derived responsibilities (already sorted by level descending)
102
- const derivedResponsibilities = job.derivedResponsibilities || [];
103
-
104
- for (const r of derivedResponsibilities) {
105
- lines.push(`- **${r.capabilityName}:** ${r.responsibility}`);
106
- }
107
- lines.push("");
108
-
109
- // Key Behaviours
110
- lines.push("## ROLE BEHAVIOURS");
111
- lines.push("");
112
-
113
76
  // Sort behaviours by maturity level (highest first)
114
77
  const sortedBehaviours = [...job.behaviourProfile].sort((a, b) => {
115
78
  const indexA = BEHAVIOUR_MATURITY_ORDER.indexOf(a.maturity);
116
79
  const indexB = BEHAVIOUR_MATURITY_ORDER.indexOf(b.maturity);
117
- // Sort in reverse order (exemplifying first, emerging last)
118
80
  if (indexA === -1 && indexB === -1) return 0;
119
81
  if (indexA === -1) return 1;
120
82
  if (indexB === -1) return -1;
121
83
  return indexB - indexA;
122
84
  });
123
85
 
124
- for (const behaviour of sortedBehaviours) {
125
- lines.push(
126
- `- **${behaviour.behaviourName}:** ${behaviour.maturityDescription || ""}`,
127
- );
128
- }
129
- lines.push("");
130
-
131
86
  // Group skills by level
132
87
  const skillsByLevel = {};
133
88
  for (const skill of job.skillMatrix) {
@@ -138,41 +93,73 @@ export function formatJobDescription({ job, discipline, grade, track }) {
138
93
  skillsByLevel[level].push(skill);
139
94
  }
140
95
 
141
- // Sort levels in a logical order using SKILL_LEVEL_ORDER from types.js
96
+ // Sort levels in reverse order (expert first, awareness last)
142
97
  const sortedLevels = Object.keys(skillsByLevel).sort((a, b) => {
143
98
  const indexA = SKILL_LEVEL_ORDER.indexOf(a.toLowerCase());
144
99
  const indexB = SKILL_LEVEL_ORDER.indexOf(b.toLowerCase());
145
- // Sort in reverse order (expert first, awareness last)
146
100
  if (indexA === -1 && indexB === -1) return a.localeCompare(b);
147
101
  if (indexA === -1) return 1;
148
102
  if (indexB === -1) return -1;
149
103
  return indexB - indexA;
150
104
  });
151
105
 
152
- for (const level of sortedLevels) {
153
- const skills = skillsByLevel[level];
154
- if (skills.length > 0) {
155
- lines.push(`## ${level.toUpperCase()}-LEVEL SKILLS`);
156
- lines.push("");
157
- // Sort skills alphabetically by name
158
- const sortedSkills = [...skills].sort((a, b) =>
159
- (a.skillName || "").localeCompare(b.skillName || ""),
160
- );
161
- for (const skill of sortedSkills) {
162
- lines.push(`- **${skill.skillName}:** ${skill.levelDescription || ""}`);
163
- }
164
- lines.push("");
165
- }
166
- }
106
+ // Keep only the top 2 skill levels for job descriptions
107
+ const topLevels = sortedLevels.slice(0, 2);
167
108
 
168
- // Qualifications
169
- lines.push("## QUALIFICATIONS");
170
- lines.push("");
109
+ // Build skill levels array for template
110
+ const skillLevels = topLevels.map((level) => {
111
+ const skills = skillsByLevel[level];
112
+ const sortedSkills = [...skills].sort((a, b) =>
113
+ (a.skillName || "").localeCompare(b.skillName || ""),
114
+ );
115
+ return {
116
+ levelHeading: `${level.toUpperCase()}-LEVEL SKILLS`,
117
+ skills: sortedSkills.map((s) => ({
118
+ skillName: s.skillName,
119
+ levelDescription: s.levelDescription || "",
120
+ })),
121
+ };
122
+ });
171
123
 
172
- if (grade.qualificationSummary) {
173
- lines.push(grade.qualificationSummary);
174
- lines.push("");
175
- }
124
+ return {
125
+ title: job.title,
126
+ gradeId: grade.id,
127
+ typicalExperienceRange: grade.typicalExperienceRange,
128
+ trackName: track?.name || null,
129
+ roleSummary,
130
+ trackRoleContext: track?.roleContext || null,
131
+ expectationsParagraph: expectationsParagraph || null,
132
+ responsibilities: (job.derivedResponsibilities || []).map((r) => ({
133
+ capabilityName: r.capabilityName,
134
+ responsibility: r.responsibility,
135
+ })),
136
+ behaviours: sortedBehaviours.map((b) => ({
137
+ behaviourName: b.behaviourName,
138
+ maturityDescription: b.maturityDescription || "",
139
+ })),
140
+ skillLevels,
141
+ qualificationSummary:
142
+ (grade.qualificationSummary || "").replace(
143
+ /\{typicalExperienceRange\}/g,
144
+ grade.typicalExperienceRange || "",
145
+ ) || null,
146
+ };
147
+ }
176
148
 
177
- return lines.join("\n");
149
+ /**
150
+ * Format job as a markdown job description using Mustache template
151
+ * @param {Object} params
152
+ * @param {Object} params.job - The job definition
153
+ * @param {Object} params.discipline - The discipline
154
+ * @param {Object} params.grade - The grade
155
+ * @param {Object} [params.track] - The track (optional)
156
+ * @param {string} template - Mustache template string
157
+ * @returns {string} Markdown formatted job description
158
+ */
159
+ export function formatJobDescription(
160
+ { job, discipline, grade, track },
161
+ template,
162
+ ) {
163
+ const data = prepareJobDescriptionData({ job, discipline, grade, track });
164
+ return Mustache.render(template, data);
178
165
  }
@@ -2,18 +2,7 @@
2
2
  * Job formatting for DOM/web output
3
3
  */
4
4
 
5
- import {
6
- div,
7
- h1,
8
- h2,
9
- p,
10
- a,
11
- span,
12
- button,
13
- section,
14
- details,
15
- summary,
16
- } from "../../lib/render.js";
5
+ import { div, h1, h2, p, a, span, button, section } from "../../lib/render.js";
17
6
  import { createBackLink } from "../../components/nav.js";
18
7
  import {
19
8
  createDetailSection,
@@ -39,6 +28,7 @@ import { formatJobDescription } from "./description.js";
39
28
  * @param {Object} [options.discipline] - Discipline entity for job description
40
29
  * @param {Object} [options.grade] - Grade entity for job description
41
30
  * @param {Object} [options.track] - Track entity for job description
31
+ * @param {string} [options.jobTemplate] - Mustache template for job description
42
32
  * @returns {HTMLElement}
43
33
  */
44
34
  export function jobToDOM(view, options = {}) {
@@ -50,9 +40,10 @@ export function jobToDOM(view, options = {}) {
50
40
  discipline,
51
41
  grade,
52
42
  track,
43
+ jobTemplate,
53
44
  } = options;
54
45
 
55
- const hasEntities = discipline && grade && track;
46
+ const hasEntities = discipline && grade && jobTemplate;
56
47
 
57
48
  return div(
58
49
  { className: "job-detail-page" },
@@ -108,6 +99,7 @@ export function jobToDOM(view, options = {}) {
108
99
  discipline,
109
100
  grade,
110
101
  track,
102
+ template: jobTemplate,
111
103
  })
112
104
  : null,
113
105
 
@@ -140,14 +132,6 @@ export function jobToDOM(view, options = {}) {
140
132
  ),
141
133
  })
142
134
  : null,
143
-
144
- // Handoff Checklists
145
- view.checklists && hasChecklistItems(view.checklists)
146
- ? createDetailSection({
147
- title: "📋 Handoff Checklists",
148
- content: createChecklistSections(view.checklists),
149
- })
150
- : null,
151
135
  )
152
136
  : null,
153
137
 
@@ -164,6 +148,7 @@ export function jobToDOM(view, options = {}) {
164
148
  discipline,
165
149
  grade,
166
150
  track,
151
+ template: jobTemplate,
167
152
  })
168
153
  : null,
169
154
  );
@@ -211,84 +196,6 @@ function getScoreColor(score) {
211
196
  return "#ef4444"; // Red
212
197
  }
213
198
 
214
- /**
215
- * Check if any checklist has items
216
- * @param {Object} checklists - Checklists object keyed by handoff type
217
- * @returns {boolean}
218
- */
219
- function hasChecklistItems(checklists) {
220
- for (const items of Object.values(checklists)) {
221
- if (items && items.length > 0) {
222
- return true;
223
- }
224
- }
225
- return false;
226
- }
227
-
228
- /**
229
- * Create collapsible checklist sections for all handoffs
230
- * @param {Object} checklists - Checklists object keyed by handoff type
231
- * @returns {HTMLElement}
232
- */
233
- function createChecklistSections(checklists) {
234
- const handoffLabels = {
235
- plan_to_code: "📋 → 💻 Plan → Code",
236
- code_to_review: "💻 → ✅ Code → Review",
237
- };
238
-
239
- const sections = Object.entries(checklists)
240
- .filter(([_, items]) => items && items.length > 0)
241
- .map(([handoff, groups]) => {
242
- const label = handoffLabels[handoff] || handoff;
243
- const totalItems = groups.reduce((sum, g) => sum + g.items.length, 0);
244
-
245
- return details(
246
- { className: "checklist-section" },
247
- summary(
248
- { className: "checklist-section-header" },
249
- span({ className: "checklist-section-label" }, label),
250
- span({ className: "badge badge-default" }, `${totalItems} items`),
251
- ),
252
- div(
253
- { className: "checklist-section-content" },
254
- ...groups.map((group) => createChecklistGroup(group)),
255
- ),
256
- );
257
- });
258
-
259
- return div({ className: "checklist-sections" }, ...sections);
260
- }
261
-
262
- /**
263
- * Create a checklist group for a capability
264
- * @param {Object} group - Group with capability, level, and items
265
- * @returns {HTMLElement}
266
- */
267
- function createChecklistGroup(group) {
268
- const emoji = group.capability.emoji || "📌";
269
- const capabilityName = group.capability.name || group.capability.id;
270
-
271
- return div(
272
- { className: "checklist-group" },
273
- div(
274
- { className: "checklist-group-header" },
275
- span({ className: "checklist-emoji" }, emoji),
276
- span({ className: "checklist-capability" }, capabilityName),
277
- span({ className: "badge badge-secondary" }, group.level),
278
- ),
279
- div(
280
- { className: "checklist-items" },
281
- ...group.items.map((item) =>
282
- div(
283
- { className: "checklist-item" },
284
- span({ className: "checklist-checkbox" }, "☐"),
285
- span({}, item),
286
- ),
287
- ),
288
- ),
289
- );
290
- }
291
-
292
199
  /**
293
200
  * Create the job description section with copy button
294
201
  * @param {Object} params
@@ -296,15 +203,25 @@ function createChecklistGroup(group) {
296
203
  * @param {Object} params.discipline - The discipline
297
204
  * @param {Object} params.grade - The grade
298
205
  * @param {Object} params.track - The track
206
+ * @param {string} params.template - Mustache template for job description
299
207
  * @returns {HTMLElement} The job description section element
300
208
  */
301
- export function createJobDescriptionSection({ job, discipline, grade, track }) {
302
- const markdown = formatJobDescription({
303
- job,
304
- discipline,
305
- grade,
306
- track,
307
- });
209
+ export function createJobDescriptionSection({
210
+ job,
211
+ discipline,
212
+ grade,
213
+ track,
214
+ template,
215
+ }) {
216
+ const markdown = formatJobDescription(
217
+ {
218
+ job,
219
+ discipline,
220
+ grade,
221
+ track,
222
+ },
223
+ template,
224
+ );
308
225
 
309
226
  const copyButton = button(
310
227
  {
@@ -388,15 +305,25 @@ export function createJobDescriptionSection({ job, discipline, grade, track }) {
388
305
  * @param {Object} params.discipline - The discipline
389
306
  * @param {Object} params.grade - The grade
390
307
  * @param {Object} params.track - The track
308
+ * @param {string} params.template - Mustache template for job description
391
309
  * @returns {HTMLElement} The job description HTML element (print-only)
392
310
  */
393
- export function createJobDescriptionHtml({ job, discipline, grade, track }) {
394
- const markdown = formatJobDescription({
395
- job,
396
- discipline,
397
- grade,
398
- track,
399
- });
311
+ export function createJobDescriptionHtml({
312
+ job,
313
+ discipline,
314
+ grade,
315
+ track,
316
+ template,
317
+ }) {
318
+ const markdown = formatJobDescription(
319
+ {
320
+ job,
321
+ discipline,
322
+ grade,
323
+ track,
324
+ },
325
+ template,
326
+ );
400
327
 
401
328
  const html = markdownToHtml(markdown);
402
329
 
@@ -15,9 +15,10 @@ import { SKILL_LEVEL_ORDER } from "../../model/levels.js";
15
15
  * Format job detail as markdown
16
16
  * @param {Object} view - Job detail view from presenter
17
17
  * @param {Object} [entities] - Original entities (for job description)
18
+ * @param {string} [jobTemplate] - Mustache template for job description
18
19
  * @returns {string}
19
20
  */
20
- export function jobToMarkdown(view, entities = {}) {
21
+ export function jobToMarkdown(view, entities = {}, jobTemplate) {
21
22
  const lines = [
22
23
  `# ${view.title}`,
23
24
  "",
@@ -77,23 +78,26 @@ export function jobToMarkdown(view, entities = {}) {
77
78
  }
78
79
 
79
80
  // Job Description (copyable markdown)
80
- if (entities.discipline && entities.grade && entities.track) {
81
+ if (entities.discipline && entities.grade && jobTemplate) {
81
82
  lines.push("---", "");
82
83
  lines.push("## Job Description", "");
83
84
  lines.push("```markdown");
84
85
  lines.push(
85
- formatJobDescription({
86
- job: {
87
- title: view.title,
88
- skillMatrix: view.skillMatrix,
89
- behaviourProfile: view.behaviourProfile,
90
- expectations: view.expectations,
91
- derivedResponsibilities: view.derivedResponsibilities,
86
+ formatJobDescription(
87
+ {
88
+ job: {
89
+ title: view.title,
90
+ skillMatrix: view.skillMatrix,
91
+ behaviourProfile: view.behaviourProfile,
92
+ expectations: view.expectations,
93
+ derivedResponsibilities: view.derivedResponsibilities,
94
+ },
95
+ discipline: entities.discipline,
96
+ grade: entities.grade,
97
+ track: entities.track,
92
98
  },
93
- discipline: entities.discipline,
94
- grade: entities.grade,
95
- track: entities.track,
96
- }),
99
+ jobTemplate,
100
+ ),
97
101
  );
98
102
  lines.push("```");
99
103
  }
@@ -36,7 +36,7 @@ export function tableToMarkdown(headers, rows) {
36
36
  export function objectToMarkdownList(obj, indent = 0) {
37
37
  const prefix = " ".repeat(indent);
38
38
  return Object.entries(obj)
39
- .map(([key, value]) => `${prefix}- **${key}**: ${value}`)
39
+ .map(([key, value]) => `${prefix}- **${capitalize(key)}**: ${value}`)
40
40
  .join("\n");
41
41
  }
42
42
 
@@ -51,12 +51,17 @@ export function formatPercent(value) {
51
51
 
52
52
  /**
53
53
  * Capitalize first letter of each word
54
+ * Handles both snake_case and camelCase
54
55
  * @param {string} str
55
56
  * @returns {string}
56
57
  */
57
58
  export function capitalize(str) {
58
59
  if (!str) return "";
59
- return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
60
+ // Insert space before uppercase letters (for camelCase), then handle snake_case
61
+ return str
62
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
63
+ .replace(/_/g, " ")
64
+ .replace(/\b\w/g, (c) => c.toUpperCase());
60
65
  }
61
66
 
62
67
  /**
package/app/handout.html CHANGED
@@ -5,6 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway - Handout View</title>
7
7
  <link rel="stylesheet" href="css/bundles/handout.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 class="slide-view handout-view">
10
17
  <header
@@ -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
+ }
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
  /**
@@ -64,3 +64,12 @@ export async function loadAgentTemplate(dataDir) {
64
64
  export async function loadSkillTemplate(dataDir) {
65
65
  return loadTemplate("skill.template.md", dataDir);
66
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
+ }
@@ -26,7 +26,10 @@ import {
26
26
  deriveAgentSkills,
27
27
  deriveReferenceGrade,
28
28
  } from "../model/agent.js";
29
- import { createSelectWithValue } from "../lib/form-controls.js";
29
+ import {
30
+ createSelectWithValue,
31
+ createDisciplineSelect,
32
+ } from "../lib/form-controls.js";
30
33
  import { createReactive } from "../lib/reactive.js";
31
34
  import { getStageEmoji } from "../formatters/stage/shared.js";
32
35
  import { formatAgentProfile } from "../formatters/agent/profile.js";
@@ -242,9 +245,9 @@ export async function renderAgentBuilder() {
242
245
  { className: "form-group" },
243
246
  label({ className: "form-label" }, "Discipline"),
244
247
  availableDisciplines.length > 0
245
- ? createSelectWithValue({
248
+ ? createDisciplineSelect({
246
249
  id: "agent-discipline-select",
247
- items: availableDisciplines,
250
+ disciplines: availableDisciplines,
248
251
  initialValue: selection.get().discipline,
249
252
  placeholder: "Select a discipline...",
250
253
  onChange: (value) => {
package/app/pages/job.js CHANGED
@@ -2,17 +2,32 @@
2
2
  * Job detail page with visualizations
3
3
  */
4
4
 
5
- import { render } from "../lib/render.js";
5
+ import { render, div, p } from "../lib/render.js";
6
6
  import { getState } from "../lib/state.js";
7
7
  import { renderError } from "../components/error-page.js";
8
8
  import { prepareJobDetail } from "../model/job.js";
9
9
  import { jobToDOM } from "../formatters/job/dom.js";
10
10
 
11
+ /** @type {string|null} Cached job template */
12
+ let jobTemplateCache = null;
13
+
14
+ /**
15
+ * Load job template with caching
16
+ * @returns {Promise<string>}
17
+ */
18
+ async function getJobTemplate() {
19
+ if (!jobTemplateCache) {
20
+ const response = await fetch("./templates/job.template.md");
21
+ jobTemplateCache = await response.text();
22
+ }
23
+ return jobTemplateCache;
24
+ }
25
+
11
26
  /**
12
27
  * Render job detail page
13
28
  * @param {Object} params - Route params
14
29
  */
15
- export function renderJobDetail(params) {
30
+ export async function renderJobDetail(params) {
16
31
  const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
17
32
  const { data } = getState();
18
33
 
@@ -63,7 +78,16 @@ export function renderJobDetail(params) {
63
78
  return;
64
79
  }
65
80
 
66
- // Format using DOM formatter
67
- const page = jobToDOM(jobView, { discipline, grade, track });
81
+ // Show loading while fetching template
82
+ render(
83
+ div(
84
+ { className: "job-detail-page" },
85
+ div({ className: "loading" }, p({}, "Loading...")),
86
+ ),
87
+ );
88
+
89
+ // Load template and format
90
+ const jobTemplate = await getJobTemplate();
91
+ const page = jobToDOM(jobView, { discipline, grade, track, jobTemplate });
68
92
  render(page);
69
93
  }
@@ -13,7 +13,10 @@ import {
13
13
  } from "../components/comparison-radar.js";
14
14
  import { createProgressionTable } from "../components/progression-table.js";
15
15
  import { renderError } from "../components/error-page.js";
16
- import { createSelectWithValue } from "../lib/form-controls.js";
16
+ import {
17
+ createSelectWithValue,
18
+ createDisciplineSelect,
19
+ } from "../lib/form-controls.js";
17
20
  import {
18
21
  prepareCurrentJob,
19
22
  prepareCustomProgression,
@@ -513,11 +516,9 @@ function createComparisonSelectorsSection({
513
516
  div(
514
517
  { className: "form-group" },
515
518
  label({ for: "compare-discipline-select" }, "Target Discipline"),
516
- createSelectWithValue({
519
+ createDisciplineSelect({
517
520
  id: "compare-discipline-select",
518
- items: data.disciplines.sort((a, b) =>
519
- a.specialization.localeCompare(b.specialization),
520
- ),
521
+ disciplines: data.disciplines,
521
522
  initialValue: selectedDisciplineId,
522
523
  placeholder: "Select discipline...",
523
524
  getDisplayName: (d) => d.specialization,
@@ -17,7 +17,7 @@ import {
17
17
  } from "../lib/render.js";
18
18
  import { getState } from "../lib/state.js";
19
19
  import { createBadge } from "../components/card.js";
20
- import { createSelectWithValue } from "../lib/form-controls.js";
20
+ import { createDisciplineSelect } from "../lib/form-controls.js";
21
21
  import {
22
22
  SKILL_LEVEL_ORDER,
23
23
  BEHAVIOUR_MATURITY_ORDER,
@@ -306,9 +306,9 @@ function renderIntroStep(data) {
306
306
  "Select a discipline to highlight which skills are most relevant for that role. " +
307
307
  "You can still assess all skills.",
308
308
  ),
309
- createSelectWithValue({
309
+ createDisciplineSelect({
310
310
  id: "discipline-filter-select",
311
- items: data.disciplines,
311
+ disciplines: data.disciplines,
312
312
  initialValue: assessmentState.discipline || "",
313
313
  placeholder: "Select discipline",
314
314
  onChange: (value) => {
package/app/slides.html CHANGED
@@ -5,6 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway - Slide View</title>
7
7
  <link rel="stylesheet" href="css/bundles/slides.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 class="slide-view">
10
17
  <header
package/bin/pathway.js CHANGED
@@ -101,7 +101,7 @@ Composite Commands:
101
101
  progress <discipline> <track> <grade> [--compare=GRADE]
102
102
  Show career progression
103
103
  questions [filters] Browse interview questions
104
- agent [<discipline> <track>] Generate AI coding agent
104
+ agent <discipline> [--track=<track>] Generate AI coding agent
105
105
 
106
106
  Global Options:
107
107
  --list Output IDs only (for piping to other commands)
@@ -121,10 +121,10 @@ Questions Filters:
121
121
  --format=FORMAT Output format: table, yaml, json
122
122
 
123
123
  Agent Options:
124
- --output=PATH Output directory (default: current directory)
125
- --preview Show output without writing files
126
- --role=ROLE Generate specific role variant
127
- --all-roles Generate default + all role variants
124
+ --track=TRACK Track for the agent (e.g., platform, forward_deployed)
125
+ --output=PATH Write files to directory (without this, outputs to console)
126
+ --stage=STAGE Generate specific stage agent (plan, code, review)
127
+ --all-stages Generate all stage agents (default)
128
128
 
129
129
  Examples:
130
130
  npx pathway skill # Summary of all skills
@@ -139,7 +139,7 @@ Examples:
139
139
  npx pathway questions --level=practitioner
140
140
  npx pathway questions --stats
141
141
 
142
- npx pathway agent software_engineering platform --output=./agents
142
+ npx pathway agent software_engineering --track=platform --output=./agents
143
143
  npx pathway --validate # Validate all data
144
144
  `;
145
145
 
@@ -160,6 +160,8 @@ function parseArgs(args) {
160
160
  type: "full",
161
161
  compare: null,
162
162
  data: null,
163
+ // Shared command options
164
+ track: null,
163
165
  // Questions command options
164
166
  level: null,
165
167
  maturity: null,
@@ -172,9 +174,6 @@ function parseArgs(args) {
172
174
  checklist: null,
173
175
  // Agent command options
174
176
  output: null,
175
- preview: false,
176
- role: null,
177
- "all-roles": false,
178
177
  stage: null,
179
178
  "all-stages": false,
180
179
  // Serve command options
@@ -196,14 +195,14 @@ function parseArgs(args) {
196
195
  result.validate = true;
197
196
  } else if (arg === "--generate-index") {
198
197
  result.generateIndex = true;
199
- } else if (arg === "--preview") {
200
- result.preview = true;
201
198
  } else if (arg.startsWith("--type=")) {
202
199
  result.type = arg.slice(7);
203
200
  } else if (arg.startsWith("--compare=")) {
204
201
  result.compare = arg.slice(10);
205
202
  } else if (arg.startsWith("--data=")) {
206
203
  result.data = arg.slice(7);
204
+ } else if (arg.startsWith("--track=")) {
205
+ result.track = arg.slice(8);
207
206
  } else if (arg.startsWith("--output=")) {
208
207
  result.output = arg.slice(9);
209
208
  } else if (arg.startsWith("--level=")) {
@@ -28,9 +28,9 @@
28
28
  managementTitle: Manager
29
29
  typicalExperienceRange: "2-5"
30
30
  ordinalRank: 2
31
- qualificationSummary:
32
- 2-5 years of relevant experience. Demonstrated ability to complete tasks
33
- independently and contribute effectively to team projects.
31
+ qualificationSummary: >
32
+ {typicalExperienceRange} years of relevant experience. Demonstrated ability
33
+ to complete tasks independently and contribute effectively to team projects.
34
34
  baseSkillLevels:
35
35
  primary: foundational
36
36
  secondary: foundational
@@ -50,9 +50,9 @@
50
50
  managementTitle: Senior Manager
51
51
  typicalExperienceRange: "5-8"
52
52
  ordinalRank: 3
53
- qualificationSummary:
54
- 5-8 years of relevant experience. Proven track record of leading technical
55
- initiatives and mentoring team members.
53
+ qualificationSummary: >
54
+ {typicalExperienceRange} years of relevant experience. Proven track record
55
+ of leading technical initiatives and mentoring team members.
56
56
  baseSkillLevels:
57
57
  primary: practitioner
58
58
  secondary: working
@@ -71,9 +71,9 @@
71
71
  managementTitle: Director
72
72
  typicalExperienceRange: "8-12"
73
73
  ordinalRank: 4
74
- qualificationSummary:
75
- 8-12 years of relevant experience. Extensive experience leading complex
76
- technical projects with multi-team impact.
74
+ qualificationSummary: >
75
+ {typicalExperienceRange} years of relevant experience. Extensive experience
76
+ leading complex technical projects with multi-team impact.
77
77
  baseSkillLevels:
78
78
  primary: expert
79
79
  secondary: practitioner
@@ -94,9 +94,10 @@
94
94
  managementTitle: VP of Engineering
95
95
  typicalExperienceRange: "12+"
96
96
  ordinalRank: 5
97
- qualificationSummary:
98
- 12+ years of relevant experience. Deep technical expertise with a proven
99
- track record of organization-wide technical leadership.
97
+ qualificationSummary: >
98
+ {typicalExperienceRange} years of relevant experience. Deep technical
99
+ expertise with a proven track record of organization-wide technical
100
+ leadership.
100
101
  baseSkillLevels:
101
102
  primary: expert
102
103
  secondary: expert
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Engineering Pathway framework for human and AI collaboration",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,47 @@
1
+ # {{title}}
2
+
3
+ - **Level:** {{gradeId}}
4
+ - **Experience:** {{typicalExperienceRange}}
5
+ {{#trackName}}- **Track:** {{trackName}}
6
+ {{/trackName}}
7
+
8
+ ## ROLE SUMMARY
9
+
10
+ {{{roleSummary}}}
11
+
12
+ {{#trackRoleContext}}
13
+ {{{trackRoleContext}}}
14
+
15
+ {{/trackRoleContext}}
16
+ {{#expectationsParagraph}}
17
+ {{{expectationsParagraph}}}
18
+
19
+ {{/expectationsParagraph}}
20
+
21
+ ## ROLE RESPONSIBILITIES
22
+
23
+ {{#responsibilities}}
24
+ - **{{capabilityName}}:** {{responsibility}}
25
+ {{/responsibilities}}
26
+
27
+ ## ROLE BEHAVIOURS
28
+
29
+ {{#behaviours}}
30
+ - **{{behaviourName}}:** {{maturityDescription}}
31
+ {{/behaviours}}
32
+
33
+ {{#skillLevels}}
34
+
35
+ ## {{levelHeading}}
36
+
37
+ {{#skills}}
38
+ - **{{skillName}}:** {{levelDescription}}
39
+ {{/skills}}
40
+
41
+ {{/skillLevels}}
42
+
43
+ ## QUALIFICATIONS
44
+
45
+ {{#qualificationSummary}}
46
+ {{{qualificationSummary}}}
47
+ {{/qualificationSummary}}