@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
@@ -7,21 +7,24 @@
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> [--output=PATH] [--preview]
12
- * npx pathway agent <discipline> <track> --stage=plan
13
- * npx pathway agent <discipline> <track> --all-stages
13
+ * npx pathway agent <discipline> [--track=<track>]
14
+ * npx pathway agent <discipline> --track=<track> --stage=plan
15
+ * npx pathway agent <discipline> --track=<track> --output=./agents
14
16
  * npx pathway agent --list
15
17
  *
16
18
  * Examples:
17
- * npx pathway agent software_engineering platform
18
- * npx pathway agent software_engineering platform --stage=plan
19
- * npx pathway agent software_engineering platform --preview
19
+ * npx pathway agent software_engineering --track=platform
20
+ * npx pathway agent software_engineering --track=platform --stage=plan
21
+ * npx pathway agent software_engineering --track=platform --output=./agents
20
22
  */
21
23
 
22
24
  import { writeFile, mkdir, readFile } from "fs/promises";
23
25
  import { join, dirname } from "path";
24
26
  import { existsSync } from "fs";
27
+ import { stringify as stringifyYaml } from "yaml";
25
28
  import { loadAgentData, loadSkillsWithAgentData } from "../model/loader.js";
26
29
  import {
27
30
  generateStageAgentProfile,
@@ -31,12 +34,13 @@ import {
31
34
  deriveAgentSkills,
32
35
  generateSkillMd,
33
36
  } from "../model/agent.js";
34
- import {
35
- formatAgentProfile,
36
- formatAgentProfileForCli,
37
- } from "../formatters/agent/profile.js";
37
+ import { formatAgentProfile } from "../formatters/agent/profile.js";
38
38
  import { formatAgentSkill } from "../formatters/agent/skill.js";
39
39
  import { formatError, formatSuccess } from "../lib/cli-output.js";
40
+ import {
41
+ loadAgentTemplate,
42
+ loadSkillTemplate,
43
+ } from "../lib/template-loader.js";
40
44
 
41
45
  /**
42
46
  * Ensure directory exists for a file path
@@ -72,6 +76,64 @@ async function generateVSCodeSettings(baseDir, vscodeSettings) {
72
76
  console.log(formatSuccess(`Updated: ${settingsPath}`));
73
77
  }
74
78
 
79
+ /**
80
+ * Generate devcontainer.json from template with VS Code settings embedded
81
+ * @param {string} baseDir - Base output directory
82
+ * @param {Object} devcontainerConfig - Devcontainer config loaded from data
83
+ * @param {Object} vscodeSettings - VS Code settings to embed in customizations
84
+ */
85
+ async function generateDevcontainer(
86
+ baseDir,
87
+ devcontainerConfig,
88
+ vscodeSettings,
89
+ ) {
90
+ if (!devcontainerConfig || Object.keys(devcontainerConfig).length === 0) {
91
+ return;
92
+ }
93
+
94
+ const devcontainerPath = join(baseDir, ".devcontainer", "devcontainer.json");
95
+
96
+ // Build devcontainer.json with VS Code settings embedded
97
+ const devcontainer = {
98
+ ...devcontainerConfig,
99
+ customizations: {
100
+ vscode: {
101
+ settings: vscodeSettings,
102
+ },
103
+ },
104
+ };
105
+
106
+ await ensureDir(devcontainerPath);
107
+ await writeFile(
108
+ devcontainerPath,
109
+ JSON.stringify(devcontainer, null, 2) + "\n",
110
+ "utf-8",
111
+ );
112
+ console.log(formatSuccess(`Created: ${devcontainerPath}`));
113
+ }
114
+
115
+ /**
116
+ * Generate GitHub Actions workflow for Copilot Coding Agent setup steps
117
+ * @param {string} baseDir - Base output directory
118
+ * @param {Object|null} copilotSetupSteps - Workflow config loaded from data
119
+ */
120
+ async function generateCopilotSetupSteps(baseDir, copilotSetupSteps) {
121
+ if (!copilotSetupSteps) {
122
+ return;
123
+ }
124
+
125
+ const workflowPath = join(
126
+ baseDir,
127
+ ".github",
128
+ "workflows",
129
+ "copilot-setup-steps.yml",
130
+ );
131
+
132
+ await ensureDir(workflowPath);
133
+ await writeFile(workflowPath, stringifyYaml(copilotSetupSteps), "utf-8");
134
+ console.log(formatSuccess(`Created: ${workflowPath}`));
135
+ }
136
+
75
137
  /**
76
138
  * Show agent summary with stats
77
139
  * @param {Object} data - Pathway data
@@ -181,10 +243,11 @@ function listAgentCombinations(data, agentData, verbose = false) {
181
243
  * Write agent profile to file
182
244
  * @param {Object} profile - Generated profile
183
245
  * @param {string} baseDir - Base output directory
246
+ * @param {string} template - Mustache template for agent profile
184
247
  */
185
- async function writeProfile(profile, baseDir) {
248
+ async function writeProfile(profile, baseDir, template) {
186
249
  const profilePath = join(baseDir, ".github", "agents", profile.filename);
187
- const profileContent = formatAgentProfile(profile);
250
+ const profileContent = formatAgentProfile(profile, template);
188
251
  await ensureDir(profilePath);
189
252
  await writeFile(profilePath, profileContent, "utf-8");
190
253
  console.log(formatSuccess(`Created: ${profilePath}`));
@@ -195,8 +258,9 @@ async function writeProfile(profile, baseDir) {
195
258
  * Write skill files
196
259
  * @param {Array} skills - Generated skills
197
260
  * @param {string} baseDir - Base output directory
261
+ * @param {string} template - Mustache template for skills
198
262
  */
199
- async function writeSkills(skills, baseDir) {
263
+ async function writeSkills(skills, baseDir, template) {
200
264
  for (const skill of skills) {
201
265
  const skillPath = join(
202
266
  baseDir,
@@ -205,7 +269,7 @@ async function writeSkills(skills, baseDir) {
205
269
  skill.dirname,
206
270
  "SKILL.md",
207
271
  );
208
- const skillContent = formatAgentSkill(skill);
272
+ const skillContent = formatAgentSkill(skill, template);
209
273
  await ensureDir(skillPath);
210
274
  await writeFile(skillPath, skillContent, "utf-8");
211
275
  console.log(formatSuccess(`Created: ${skillPath}`));
@@ -216,7 +280,7 @@ async function writeSkills(skills, baseDir) {
216
280
  * Run the agent command
217
281
  * @param {Object} params - Command parameters
218
282
  * @param {Object} params.data - Loaded pathway data
219
- * @param {string[]} params.args - Command arguments [discipline_id, track_id]
283
+ * @param {string[]} params.args - Command arguments [discipline_id]
220
284
  * @param {Object} params.options - Command options
221
285
  * @param {string} params.dataDir - Path to data directory
222
286
  */
@@ -237,11 +301,14 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
237
301
  return;
238
302
  }
239
303
 
240
- const [disciplineId, trackId] = args;
304
+ const [disciplineId] = args;
305
+ const trackId = options.track;
241
306
 
242
- if (!disciplineId || !trackId) {
307
+ if (!disciplineId) {
243
308
  console.error(
244
- formatError("Usage: npx pathway agent <discipline_id> <track_id>"),
309
+ formatError(
310
+ "Usage: npx pathway agent <discipline_id> [--track=<track_id>]",
311
+ ),
245
312
  );
246
313
  console.error(
247
314
  "\nRun 'npx pathway agent --list' to see available combinations.",
@@ -251,7 +318,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
251
318
 
252
319
  // Look up human definitions
253
320
  const humanDiscipline = data.disciplines.find((d) => d.id === disciplineId);
254
- const humanTrack = data.tracks.find((t) => t.id === trackId);
321
+ const humanTrack = trackId ? data.tracks.find((t) => t.id === trackId) : null;
255
322
 
256
323
  if (!humanDiscipline) {
257
324
  console.error(formatError(`Unknown discipline: ${disciplineId}`));
@@ -262,7 +329,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
262
329
  process.exit(1);
263
330
  }
264
331
 
265
- if (!humanTrack) {
332
+ if (trackId && !humanTrack) {
266
333
  console.error(formatError(`Unknown track: ${trackId}`));
267
334
  console.error("\nAvailable tracks:");
268
335
  for (const t of data.tracks) {
@@ -275,7 +342,9 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
275
342
  const agentDiscipline = agentData.disciplines.find(
276
343
  (d) => d.id === disciplineId,
277
344
  );
278
- const agentTrack = agentData.tracks.find((t) => t.id === trackId);
345
+ const agentTrack = trackId
346
+ ? agentData.tracks.find((t) => t.id === trackId)
347
+ : null;
279
348
 
280
349
  if (!agentDiscipline) {
281
350
  console.error(
@@ -288,7 +357,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
288
357
  process.exit(1);
289
358
  }
290
359
 
291
- if (!agentTrack) {
360
+ if (trackId && !agentTrack) {
292
361
  console.error(formatError(`No agent definition for track: ${trackId}`));
293
362
  console.error("\nAgent definitions exist for:");
294
363
  for (const t of agentData.tracks) {
@@ -340,14 +409,23 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
340
409
  process.exit(1);
341
410
  }
342
411
 
343
- // Preview or write
344
- if (options.preview) {
345
- 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));
346
418
  return;
347
419
  }
348
420
 
349
- await writeProfile(profile, baseDir);
421
+ await writeProfile(profile, baseDir, agentTemplate);
350
422
  await generateVSCodeSettings(baseDir, agentData.vscodeSettings);
423
+ await generateDevcontainer(
424
+ baseDir,
425
+ agentData.devcontainer,
426
+ agentData.vscodeSettings,
427
+ );
428
+ await generateCopilotSetupSteps(baseDir, agentData.copilotSetupSteps);
351
429
  console.log("");
352
430
  console.log(
353
431
  formatSuccess(`Generated stage agent: ${profile.frontmatter.name}`),
@@ -406,20 +484,30 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
406
484
  }
407
485
  }
408
486
 
409
- // Preview or write
410
- 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) {
411
493
  for (const profile of profiles) {
412
- console.log(formatAgentProfileForCli(profile));
494
+ console.log(formatAgentProfile(profile, agentTemplate));
413
495
  console.log("\n---\n");
414
496
  }
415
497
  return;
416
498
  }
417
499
 
418
500
  for (const profile of profiles) {
419
- await writeProfile(profile, baseDir);
501
+ await writeProfile(profile, baseDir, agentTemplate);
420
502
  }
421
- await writeSkills(skillFiles, baseDir);
503
+ await writeSkills(skillFiles, baseDir, skillTemplate);
422
504
  await generateVSCodeSettings(baseDir, agentData.vscodeSettings);
505
+ await generateDevcontainer(
506
+ baseDir,
507
+ agentData.devcontainer,
508
+ agentData.vscodeSettings,
509
+ );
510
+ await generateCopilotSetupSteps(baseDir, agentData.copilotSetupSteps);
423
511
 
424
512
  console.log("");
425
513
  console.log(formatSuccess(`Generated ${profiles.length} agents:`));
@@ -157,8 +157,8 @@ function handleDetail({
157
157
  * @param {Object} config - Command configuration
158
158
  * @param {string} config.commandName - Command name for error messages
159
159
  * @param {string[]} config.requiredArgs - Array of required argument names
160
- * @param {Function} config.findEntities - Function to find entities: (data, args) => entities object
161
- * @param {Function} config.validateEntities - Function to validate entities: (entities, data) => error string | null
160
+ * @param {Function} config.findEntities - Function to find entities: (data, args, options) => entities object
161
+ * @param {Function} config.validateEntities - Function to validate entities: (entities, data, options) => error string | null
162
162
  * @param {Function} config.presenter - Function to present data: (entities, data, options) => view
163
163
  * @param {Function} config.formatter - Function to format output: (view, options, data) => void
164
164
  * @param {string} [config.usageExample] - Optional usage example
@@ -184,7 +184,7 @@ export function createCompositeCommand({
184
184
  }
185
185
 
186
186
  const entities = findEntities(data, args, options);
187
- const validationError = validateEntities(entities, data);
187
+ const validationError = validateEntities(entities, data, options);
188
188
 
189
189
  if (validationError) {
190
190
  console.error(validationError);
@@ -2,6 +2,11 @@
2
2
  * Interview CLI Command
3
3
  *
4
4
  * Generates and displays interview questions in the terminal.
5
+ *
6
+ * Usage:
7
+ * npx pathway interview <discipline> <grade> # Interview for trackless job
8
+ * npx pathway interview <discipline> <grade> --track=<track> # Interview with track
9
+ * npx pathway interview <discipline> <grade> --track=<track> --type=short
5
10
  */
6
11
 
7
12
  import { createCompositeCommand } from "./command-factory.js";
@@ -22,7 +27,7 @@ function formatInterview(view, options) {
22
27
 
23
28
  export const runInterviewCommand = createCompositeCommand({
24
29
  commandName: "interview",
25
- requiredArgs: ["discipline_id", "track_id", "grade_id"],
30
+ requiredArgs: ["discipline_id", "grade_id"],
26
31
  findEntities: (data, args, options) => {
27
32
  const interviewType = options.type || "full";
28
33
 
@@ -34,20 +39,22 @@ export const runInterviewCommand = createCompositeCommand({
34
39
 
35
40
  return {
36
41
  discipline: data.disciplines.find((d) => d.id === args[0]),
37
- track: data.tracks.find((t) => t.id === args[1]),
38
- grade: data.grades.find((g) => g.id === args[2]),
42
+ grade: data.grades.find((g) => g.id === args[1]),
43
+ track: options.track
44
+ ? data.tracks.find((t) => t.id === options.track)
45
+ : null,
39
46
  interviewType,
40
47
  };
41
48
  },
42
- validateEntities: (entities, _data) => {
49
+ validateEntities: (entities, _data, options) => {
43
50
  if (!entities.discipline) {
44
51
  return `Discipline not found: ${entities.discipline}`;
45
52
  }
46
53
  if (!entities.grade) {
47
54
  return `Grade not found: ${entities.grade}`;
48
55
  }
49
- if (!entities.track) {
50
- return `Track not found: ${entities.track}`;
56
+ if (options.track && !entities.track) {
57
+ return `Track not found: ${options.track}`;
51
58
  }
52
59
  return null;
53
60
  },
@@ -64,5 +71,5 @@ export const runInterviewCommand = createCompositeCommand({
64
71
  formatter: (view, options, data) =>
65
72
  formatInterview(view, { ...options, framework: data.framework }),
66
73
  usageExample:
67
- "npx pathway interview software_engineering platform L4 --type=short",
74
+ "npx pathway interview software_engineering L4 --track=platform --type=short",
68
75
  });
@@ -4,11 +4,12 @@
4
4
  * Generates and displays job definitions in the terminal.
5
5
  *
6
6
  * Usage:
7
- * npx pathway job # Summary with stats
8
- * npx pathway job --list # All valid combinations (for piping)
9
- * npx pathway job <discipline> <track> <grade> # Detail view
10
- * npx pathway job se platform L3 --checklist=code_to_review # Show checklist for handoff
11
- * npx pathway job --validate # Validation checks
7
+ * npx pathway job # Summary with stats
8
+ * npx pathway job --list # All valid combinations (for piping)
9
+ * npx pathway job <discipline> <grade> # Detail view (trackless)
10
+ * npx pathway job <discipline> <grade> --track=<track> # Detail view (with track)
11
+ * npx pathway job se L3 --track=platform --checklist=code # Show checklist for handoff
12
+ * npx pathway job --validate # Validation checks
12
13
  */
13
14
 
14
15
  import { prepareJobDetail } from "../model/job.js";
@@ -19,15 +20,17 @@ import {
19
20
  deriveChecklist,
20
21
  formatChecklistMarkdown,
21
22
  } from "../model/checklist.js";
23
+ import { loadJobTemplate } from "../lib/template-loader.js";
22
24
 
23
25
  /**
24
26
  * Format job output
25
27
  * @param {Object} view - Presenter view
26
28
  * @param {Object} _options - Command options
27
29
  * @param {Object} entities - Original entities
30
+ * @param {string} jobTemplate - Mustache template for job description
28
31
  */
29
- function formatJob(view, _options, entities) {
30
- console.log(jobToMarkdown(view, entities));
32
+ function formatJob(view, _options, entities, jobTemplate) {
33
+ console.log(jobToMarkdown(view, entities, jobTemplate));
31
34
  }
32
35
 
33
36
  /**
@@ -36,8 +39,9 @@ function formatJob(view, _options, entities) {
36
39
  * @param {Object} params.data - All loaded data
37
40
  * @param {string[]} params.args - Command arguments
38
41
  * @param {Object} params.options - Command options
42
+ * @param {string} params.dataDir - Path to data directory
39
43
  */
40
- export async function runJobCommand({ data, args, options }) {
44
+ export async function runJobCommand({ data, args, options, dataDir }) {
41
45
  const jobs = generateAllJobs({
42
46
  disciplines: data.disciplines,
43
47
  grades: data.grades,
@@ -47,10 +51,14 @@ export async function runJobCommand({ data, args, options }) {
47
51
  validationRules: data.framework.validationRules,
48
52
  });
49
53
 
50
- // --list: Output clean lines for piping
54
+ // --list: Output clean lines for piping (discipline grade track format)
51
55
  if (options.list) {
52
56
  for (const job of jobs) {
53
- console.log(`${job.discipline.id} ${job.track.id} ${job.grade.id}`);
57
+ if (job.track) {
58
+ console.log(`${job.discipline.id} ${job.grade.id} ${job.track.id}`);
59
+ } else {
60
+ console.log(`${job.discipline.id} ${job.grade.id}`);
61
+ }
54
62
  }
55
63
  return;
56
64
  }
@@ -71,22 +79,29 @@ export async function runJobCommand({ data, args, options }) {
71
79
  console.log(`\nTotal: ${jobs.length} valid job combinations`);
72
80
  console.log(`\nRun 'npx pathway job --list' for all combinations`);
73
81
  console.log(
74
- `Run 'npx pathway job <discipline> <track> <grade>' for details\n`,
82
+ `Run 'npx pathway job <discipline> <grade> [--track=<track>]' for details\n`,
75
83
  );
76
84
  return;
77
85
  }
78
86
 
79
- // Handle job detail view
80
- if (args.length < 3) {
81
- console.error("Usage: npx pathway job <discipline> <track> <grade>");
87
+ // Handle job detail view - requires discipline and grade
88
+ if (args.length < 2) {
89
+ console.error(
90
+ "Usage: npx pathway job <discipline> <grade> [--track=<track>]",
91
+ );
82
92
  console.error(" npx pathway job --list");
83
- console.error("Example: npx pathway job software_engineering platform L4");
93
+ console.error("Example: npx pathway job software_engineering L4");
94
+ console.error(
95
+ "Example: npx pathway job software_engineering L4 --track=platform",
96
+ );
84
97
  process.exit(1);
85
98
  }
86
99
 
87
100
  const discipline = data.disciplines.find((d) => d.id === args[0]);
88
- const track = data.tracks.find((t) => t.id === args[1]);
89
- const grade = data.grades.find((g) => g.id === args[2]);
101
+ const grade = data.grades.find((g) => g.id === args[1]);
102
+ const track = options.track
103
+ ? data.tracks.find((t) => t.id === options.track)
104
+ : null;
90
105
 
91
106
  if (!discipline) {
92
107
  console.error(`Discipline not found: ${args[0]}`);
@@ -94,15 +109,15 @@ export async function runJobCommand({ data, args, options }) {
94
109
  process.exit(1);
95
110
  }
96
111
 
97
- if (!track) {
98
- console.error(`Track not found: ${args[1]}`);
99
- console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
112
+ if (!grade) {
113
+ console.error(`Grade not found: ${args[1]}`);
114
+ console.error(`Available: ${data.grades.map((g) => g.id).join(", ")}`);
100
115
  process.exit(1);
101
116
  }
102
117
 
103
- if (!grade) {
104
- console.error(`Grade not found: ${args[2]}`);
105
- console.error(`Available: ${data.grades.map((g) => g.id).join(", ")}`);
118
+ if (options.track && !track) {
119
+ console.error(`Track not found: ${options.track}`);
120
+ console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
106
121
  process.exit(1);
107
122
  }
108
123
 
@@ -126,32 +141,36 @@ export async function runJobCommand({ data, args, options }) {
126
141
  return;
127
142
  }
128
143
 
129
- // --checklist: Show checklist for a specific handoff
144
+ // --checklist: Show checklist for a specific stage
130
145
  if (options.checklist) {
131
- const validHandoffs = ["plan_to_code", "code_to_review"];
132
- if (!validHandoffs.includes(options.checklist)) {
133
- console.error(`Invalid handoff: ${options.checklist}`);
134
- console.error(`Available: ${validHandoffs.join(", ")}`);
146
+ const validStages = ["plan", "code"];
147
+ if (!validStages.includes(options.checklist)) {
148
+ console.error(`Invalid stage: ${options.checklist}`);
149
+ console.error(`Available: ${validStages.join(", ")}`);
135
150
  process.exit(1);
136
151
  }
137
152
 
138
153
  const checklist = deriveChecklist({
139
- handoff: options.checklist,
154
+ stageId: options.checklist,
140
155
  skillMatrix: view.skillMatrix,
156
+ skills: data.skills,
141
157
  capabilities: data.capabilities,
142
158
  });
143
159
 
144
160
  if (checklist.length === 0) {
145
- console.log(`\nNo checklist items for ${options.checklist}\n`);
161
+ console.log(`\nNo checklist items for ${options.checklist} stage\n`);
146
162
  return;
147
163
  }
148
164
 
149
- const handoffLabel = options.checklist.replace(/_/g, " → ");
150
- console.log(`\n# ${view.title} ${handoffLabel}\n`);
165
+ const stageLabel =
166
+ options.checklist.charAt(0).toUpperCase() + options.checklist.slice(1);
167
+ console.log(`\n# ${view.title} — ${stageLabel} Stage Checklist\n`);
151
168
  console.log(formatChecklistMarkdown(checklist));
152
169
  console.log("");
153
170
  return;
154
171
  }
155
172
 
156
- 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);
157
176
  }
@@ -2,6 +2,11 @@
2
2
  * Progress CLI Command
3
3
  *
4
4
  * Shows career progression analysis in the terminal.
5
+ *
6
+ * Usage:
7
+ * npx pathway progress <discipline> <grade> # Progress for trackless job
8
+ * npx pathway progress <discipline> <grade> --track=<track> # Progress with track
9
+ * npx pathway progress <discipline> <from_grade> --compare=<to_grade> # Compare grades
5
10
  */
6
11
 
7
12
  import { createCompositeCommand } from "./command-factory.js";
@@ -21,11 +26,13 @@ function formatProgress(view) {
21
26
 
22
27
  export const runProgressCommand = createCompositeCommand({
23
28
  commandName: "progress",
24
- requiredArgs: ["discipline_id", "track_id", "grade_id"],
29
+ requiredArgs: ["discipline_id", "grade_id"],
25
30
  findEntities: (data, args, options) => {
26
31
  const discipline = data.disciplines.find((d) => d.id === args[0]);
27
- const track = data.tracks.find((t) => t.id === args[1]);
28
- const grade = data.grades.find((g) => g.id === args[2]);
32
+ const grade = data.grades.find((g) => g.id === args[1]);
33
+ const track = options.track
34
+ ? data.tracks.find((t) => t.id === options.track)
35
+ : null;
29
36
 
30
37
  let targetGrade;
31
38
  if (options.compare) {
@@ -44,15 +51,15 @@ export const runProgressCommand = createCompositeCommand({
44
51
 
45
52
  return { discipline, grade, track, targetGrade };
46
53
  },
47
- validateEntities: (entities, _data) => {
54
+ validateEntities: (entities, _data, options) => {
48
55
  if (!entities.discipline) {
49
56
  return `Discipline not found`;
50
57
  }
51
58
  if (!entities.grade) {
52
59
  return `Grade not found`;
53
60
  }
54
- if (!entities.track) {
55
- return `Track not found`;
61
+ if (options.track && !entities.track) {
62
+ return `Track not found: ${options.track}`;
56
63
  }
57
64
  if (!entities.targetGrade) {
58
65
  return `Target grade not found`;
@@ -73,5 +80,5 @@ export const runProgressCommand = createCompositeCommand({
73
80
  }),
74
81
  formatter: formatProgress,
75
82
  usageExample:
76
- "npx pathway progress software_engineering platform L3 --compare=L4",
83
+ "npx pathway progress software_engineering L3 --track=platform --compare=L4",
77
84
  });
@@ -15,6 +15,7 @@ import { loadFrameworkConfig } from "../model/loader.js";
15
15
  const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = dirname(__filename);
17
17
  const publicDir = join(__dirname, "..");
18
+ const rootDir = join(__dirname, "../..");
18
19
 
19
20
  const MIME_TYPES = {
20
21
  ".html": "text/html; charset=utf-8",
@@ -23,6 +24,7 @@ const MIME_TYPES = {
23
24
  ".yaml": "text/yaml; charset=utf-8",
24
25
  ".yml": "text/yaml; charset=utf-8",
25
26
  ".json": "application/json; charset=utf-8",
27
+ ".md": "text/markdown; charset=utf-8",
26
28
  ".svg": "image/svg+xml",
27
29
  ".png": "image/png",
28
30
  ".ico": "image/x-icon",
@@ -113,6 +115,9 @@ export async function runServeCommand({ dataDir, options }) {
113
115
  if (pathname.startsWith("/data/")) {
114
116
  // Serve from user's data directory
115
117
  filePath = join(dataDir, pathname.slice(6));
118
+ } else if (pathname.startsWith("/templates/")) {
119
+ // Serve from templates directory
120
+ filePath = join(rootDir, pathname);
116
121
  } else if (pathname === "/" || pathname === "") {
117
122
  // Serve index.html for root
118
123
  filePath = join(publicDir, "index.html");
@@ -58,18 +58,8 @@ function formatDetail(viewAndContext, _framework) {
58
58
  const emoji = getStageEmoji(stages, stage.id);
59
59
 
60
60
  console.log(formatHeader(`\n${emoji} ${view.name}\n`));
61
- console.log(`Mode: ${view.modeBadge}\n`);
62
61
  console.log(`${view.description}\n`);
63
62
 
64
- // Tools
65
- if (view.tools.length > 0) {
66
- console.log(formatSubheader("Tools\n"));
67
- for (const tool of view.tools) {
68
- console.log(formatBullet(`${tool.icon} ${tool.label}`, 1));
69
- }
70
- console.log();
71
- }
72
-
73
63
  // Entry criteria
74
64
  if (view.entryCriteria.length > 0) {
75
65
  console.log(formatSubheader("Entry Criteria\n"));
@@ -12,13 +12,13 @@
12
12
 
13
13
  import { createEntityCommand } from "./command-factory.js";
14
14
  import { trackToMarkdown } from "../formatters/track/markdown.js";
15
- import { sortTracksByType } from "../formatters/track/shared.js";
15
+ import { sortTracksByName } from "../formatters/track/shared.js";
16
16
  import { formatTable } from "../lib/cli-output.js";
17
17
  import { getConceptEmoji } from "../model/levels.js";
18
18
 
19
19
  /**
20
20
  * Format track summary output
21
- * @param {Array} tracks - Raw track entities (already sorted by type)
21
+ * @param {Array} tracks - Raw track entities
22
22
  * @param {Object} data - Full data context
23
23
  */
24
24
  function formatSummary(tracks, data) {
@@ -28,14 +28,11 @@ function formatSummary(tracks, data) {
28
28
  console.log(`\n${emoji} Tracks\n`);
29
29
 
30
30
  const rows = tracks.map((t) => {
31
- const types = [];
32
- if (t.isProfessional) types.push("P");
33
- if (t.isManagement) types.push("M");
34
31
  const modCount = Object.keys(t.skillModifiers || {}).length;
35
- return [t.id, t.name, types.join("/") || "-", modCount];
32
+ return [t.id, t.name, modCount];
36
33
  });
37
34
 
38
- console.log(formatTable(["ID", "Name", "Type", "Modifiers"], rows));
35
+ console.log(formatTable(["ID", "Name", "Modifiers"], rows));
39
36
  console.log(`\nTotal: ${tracks.length} tracks`);
40
37
  console.log(`\nRun 'npx pathway track --list' for IDs`);
41
38
  console.log(`Run 'npx pathway track <id>' for details\n`);
@@ -63,7 +60,7 @@ export const runTrackCommand = createEntityCommand({
63
60
  behaviours: data.behaviours,
64
61
  disciplines: data.disciplines,
65
62
  }),
66
- sortItems: sortTracksByType,
63
+ sortItems: sortTracksByName,
67
64
  formatSummary,
68
65
  formatDetail,
69
66
  emoji: "🛤️",