@forwardimpact/pathway 0.22.0 → 0.23.1

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 (78) hide show
  1. package/bin/fit-pathway.js +8 -4
  2. package/package.json +6 -2
  3. package/src/commands/agent.js +6 -3
  4. package/src/commands/behaviour.js +11 -1
  5. package/src/commands/build.js +11 -2
  6. package/src/commands/command-factory.js +4 -2
  7. package/src/commands/dev.js +9 -2
  8. package/src/commands/discipline.js +25 -10
  9. package/src/commands/driver.js +11 -1
  10. package/src/commands/job.js +127 -28
  11. package/src/commands/level.js +25 -3
  12. package/src/commands/skill.js +11 -1
  13. package/src/commands/stage.js +11 -1
  14. package/src/commands/tool.js +6 -3
  15. package/src/commands/track.js +20 -4
  16. package/src/components/card.js +8 -104
  17. package/src/components/comparison-radar.js +1 -1
  18. package/src/components/detail.js +16 -118
  19. package/src/components/error-page.js +8 -68
  20. package/src/components/grid.js +12 -106
  21. package/src/components/list.js +7 -116
  22. package/src/components/nav.js +7 -60
  23. package/src/css/bundles/app.css +25 -21
  24. package/src/css/bundles/handout.css +33 -33
  25. package/src/css/bundles/slides.css +25 -25
  26. package/src/formatters/discipline/markdown.js +16 -1
  27. package/src/formatters/interview/shared.js +3 -3
  28. package/src/formatters/job/description.js +2 -2
  29. package/src/formatters/progress/shared.js +3 -3
  30. package/src/formatters/skill/shared.js +1 -1
  31. package/src/formatters/track/markdown.js +14 -0
  32. package/src/formatters/track/shared.js +1 -1
  33. package/src/handout.html +32 -13
  34. package/src/index.html +32 -13
  35. package/src/lib/error-boundary.js +3 -66
  36. package/src/lib/errors.js +7 -45
  37. package/src/lib/job-cache.js +1 -1
  38. package/src/lib/markdown.js +2 -109
  39. package/src/lib/reactive.js +7 -73
  40. package/src/lib/render.js +49 -197
  41. package/src/lib/router-core.js +2 -156
  42. package/src/lib/router-pages.js +2 -11
  43. package/src/lib/router-slides.js +2 -197
  44. package/src/lib/state.js +14 -63
  45. package/src/lib/utils.js +3 -10
  46. package/src/lib/yaml-loader.js +13 -71
  47. package/src/pages/agent-builder.js +1 -1
  48. package/src/pages/assessment-results.js +1 -1
  49. package/src/pages/job-builder.js +1 -1
  50. package/src/pages/job.js +1 -1
  51. package/src/pages/skill.js +1 -1
  52. package/src/slide-main.js +1 -1
  53. package/src/slides/index.js +1 -1
  54. package/src/slides/job.js +1 -1
  55. package/src/slides/overview.js +1 -1
  56. package/src/slides.html +32 -13
  57. package/src/css/base.css +0 -56
  58. package/src/css/components/badges.css +0 -232
  59. package/src/css/components/buttons.css +0 -101
  60. package/src/css/components/forms.css +0 -191
  61. package/src/css/components/layout.css +0 -218
  62. package/src/css/components/nav.css +0 -206
  63. package/src/css/components/progress.css +0 -166
  64. package/src/css/components/states.css +0 -82
  65. package/src/css/components/surfaces.css +0 -347
  66. package/src/css/components/tables.css +0 -362
  67. package/src/css/components/top-bar.css +0 -180
  68. package/src/css/components/typography.css +0 -121
  69. package/src/css/components/utilities.css +0 -41
  70. package/src/css/pages/detail.css +0 -119
  71. package/src/css/reset.css +0 -50
  72. package/src/css/tokens.css +0 -162
  73. package/src/css/views/handout.css +0 -30
  74. package/src/css/views/print.css +0 -634
  75. package/src/css/views/slide-animations.css +0 -113
  76. package/src/css/views/slide-base.css +0 -331
  77. package/src/css/views/slide-sections.css +0 -597
  78. package/src/css/views/slide-tables.css +0 -275
@@ -117,7 +117,9 @@ Generate job definitions from discipline × level × track combinations.
117
117
 
118
118
  Usage:
119
119
  npx fit-pathway job Summary with stats
120
+ npx fit-pathway job --track=<track> Summary filtered by track
120
121
  npx fit-pathway job --list All valid combinations
122
+ npx fit-pathway job --list --track=<track> Combinations for a track
121
123
  npx fit-pathway job <discipline> <level> Detail view (trackless)
122
124
  npx fit-pathway job <d> <l> --track=<track> Detail view (with track)
123
125
  npx fit-pathway job <d> <l> --skills Plain list of skill IDs
@@ -126,15 +128,17 @@ Usage:
126
128
 
127
129
  Options:
128
130
  --track=TRACK Track specialization (e.g., platform, forward_deployed)
131
+ Also filters --list and summary modes
129
132
  --skills Output plain list of skill IDs (for piping)
130
133
  --tools Output plain list of tool names (for piping)
131
134
  --checklist=STAGE Show checklist for stage handoff (plan, code)
132
135
 
133
136
  Examples:
134
- npx fit-pathway job software_engineering L4
135
- npx fit-pathway job software_engineering L4 --track=platform
136
- npx fit-pathway job se L3 --track=platform --skills
137
- npx fit-pathway job se L3 --track=platform --tools
137
+ npx fit-pathway job # overview of all jobs
138
+ npx fit-pathway job --track=forward_deployed # jobs on a specific track
139
+ npx fit-pathway job --list --track=forward_deployed # list for piping
140
+ npx fit-pathway job software_engineering J060 # trackless job detail
141
+ npx fit-pathway job software_engineering J060 --track=platform # with track
138
142
 
139
143
  ────────────────────────────────────────────────────────────────────────────────
140
144
  AGENT COMMAND
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "Career progression web app and CLI for exploring roles and generating agent teams",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,12 +41,16 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@forwardimpact/map": "^0.11.0",
44
- "@forwardimpact/libpathway": "^3.0.0",
44
+ "@forwardimpact/libskill": "^3.0.0",
45
+ "@forwardimpact/libui": "^1.0.0",
45
46
  "mustache": "^4.2.0",
46
47
  "simple-icons": "^16.7.0",
47
48
  "yaml": "^2.3.4"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=18.0.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
51
55
  }
52
56
  }
@@ -42,7 +42,7 @@ import {
42
42
  buildAgentIndex,
43
43
  getDisciplineAbbreviation,
44
44
  toKebabCase,
45
- } from "@forwardimpact/libpathway";
45
+ } from "@forwardimpact/libskill";
46
46
  import { formatAgentProfile } from "../formatters/agent/profile.js";
47
47
  import {
48
48
  formatAgentSkill,
@@ -199,7 +199,7 @@ function showAgentSummary(data, agentData, skillsWithAgent) {
199
199
  */
200
200
  function listAgentCombinations(data, agentData, verbose = false) {
201
201
  if (!verbose) {
202
- // Clean output for piping
202
+ // Descriptive output for piping and AI agent discovery
203
203
  for (const discipline of agentData.disciplines) {
204
204
  for (const track of agentData.tracks) {
205
205
  const humanDiscipline = data.disciplines.find(
@@ -209,7 +209,10 @@ function listAgentCombinations(data, agentData, verbose = false) {
209
209
  if (humanDiscipline && humanTrack) {
210
210
  const abbrev = getDisciplineAbbreviation(discipline.id);
211
211
  const agentName = `${abbrev}-${toKebabCase(track.id)}`;
212
- console.log(`${agentName} ${discipline.id} ${track.id}`);
212
+ const specName = humanDiscipline.specialization || humanDiscipline.id;
213
+ console.log(
214
+ `${agentName} ${discipline.id} ${track.id}, ${specName} (${humanTrack.name})`,
215
+ );
213
216
  }
214
217
  }
215
218
  }
@@ -14,6 +14,15 @@ import { createEntityCommand } from "./command-factory.js";
14
14
  import { behaviourToMarkdown } from "../formatters/behaviour/markdown.js";
15
15
  import { formatTable } from "../lib/cli-output.js";
16
16
 
17
+ /**
18
+ * Format behaviour list item for --list output
19
+ * @param {Object} behaviour - Behaviour entity
20
+ * @returns {string} Formatted list line
21
+ */
22
+ function formatListItem(behaviour) {
23
+ return `${behaviour.id}, ${behaviour.name}`;
24
+ }
25
+
17
26
  /**
18
27
  * Format behaviour summary output
19
28
  * @param {Array} behaviours - Raw behaviour entities
@@ -34,7 +43,7 @@ function formatSummary(behaviours, data) {
34
43
 
35
44
  console.log(formatTable(["ID", "Name", "Drivers"], rows));
36
45
  console.log(`\nTotal: ${behaviours.length} behaviours`);
37
- console.log(`\nRun 'npx pathway behaviour --list' for IDs`);
46
+ console.log(`\nRun 'npx pathway behaviour --list' for IDs and names`);
38
47
  console.log(`Run 'npx pathway behaviour <id>' for details\n`);
39
48
  }
40
49
 
@@ -57,5 +66,6 @@ export const runBehaviourCommand = createEntityCommand({
57
66
  }),
58
67
  formatSummary,
59
68
  formatDetail,
69
+ formatListItem,
60
70
  emojiIcon: "🧠",
61
71
  });
@@ -42,7 +42,8 @@ function resolvePackageLib(packageName) {
42
42
  }
43
43
 
44
44
  const mapLibDir = resolvePackageLib("@forwardimpact/map");
45
- const modelLibDir = resolvePackageLib("@forwardimpact/libpathway");
45
+ const modelLibDir = resolvePackageLib("@forwardimpact/libskill");
46
+ const uiLibDir = resolvePackageLib("@forwardimpact/libui");
46
47
 
47
48
  /**
48
49
  * Files and directories to copy from app/
@@ -141,13 +142,21 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
141
142
  }
142
143
  }
143
144
 
144
- // Copy @forwardimpact/map and @forwardimpact/libpathway packages
145
+ // Copy @forwardimpact/map and @forwardimpact/libskill packages
145
146
  // These are needed by the browser's import map
146
147
  console.log("📚 Copying package dependencies...");
147
148
  await cp(mapLibDir, join(outputDir, "map/lib"), { recursive: true });
148
149
  console.log(` ✓ map/lib`);
149
150
  await cp(modelLibDir, join(outputDir, "model/lib"), { recursive: true });
150
151
  console.log(` ✓ model/lib`);
152
+ // Copy libui JS (src/) and CSS (src/css/)
153
+ await cp(uiLibDir, join(outputDir, "ui/lib"), { recursive: true });
154
+ // CSS is within uiLibDir/css/ so it's already copied as ui/lib/css/
155
+ // Create ui/css/ symlink-like copy for the CSS @import paths
156
+ await cp(join(uiLibDir, "css"), join(outputDir, "ui/css"), {
157
+ recursive: true,
158
+ });
159
+ console.log(` ✓ ui/lib + ui/css`);
151
160
 
152
161
  // Copy data directory (dereference symlinks to copy actual content)
153
162
  console.log("📁 Copying data files...");
@@ -22,6 +22,7 @@ import { capitalize } from "../formatters/shared.js";
22
22
  * @param {Function} config.presentDetail - Function to present detail: (entity, data, options) => view
23
23
  * @param {Function} config.formatSummary - Function to format summary output: (items, data) => void
24
24
  * @param {Function} config.formatDetail - Function to format detail output: (view, framework) => void
25
+ * @param {Function} [config.formatListItem] - Optional function to format list item: (item) => string (defaults to item.id)
25
26
  * @param {Function} [config.sortItems] - Optional function to sort items: (items) => sortedItems
26
27
  * @param {Function} [config.validate] - Optional validation function: (data) => {errors: [], warnings: []}
27
28
  * @param {string} [config.emojiIcon] - Optional emoji for the entity
@@ -34,6 +35,7 @@ export function createEntityCommand({
34
35
  presentDetail,
35
36
  formatSummary,
36
37
  formatDetail,
38
+ formatListItem,
37
39
  sortItems,
38
40
  validate,
39
41
  _emojiIcon = "",
@@ -48,10 +50,10 @@ export function createEntityCommand({
48
50
  return handleValidate({ data, entityName, pluralName, validate });
49
51
  }
50
52
 
51
- // --list: Output clean newline-separated IDs for piping
53
+ // --list: Output descriptive comma-separated lines for piping and AI agent discovery
52
54
  if (options.list) {
53
55
  for (const item of items) {
54
- console.log(item.id);
56
+ console.log(formatListItem ? formatListItem(item) : item.id);
55
57
  }
56
58
  return;
57
59
  }
@@ -31,7 +31,8 @@ function resolvePackageLib(packageName) {
31
31
  }
32
32
 
33
33
  const mapLibDir = resolvePackageLib("@forwardimpact/map");
34
- const modelLibDir = resolvePackageLib("@forwardimpact/libpathway");
34
+ const modelLibDir = resolvePackageLib("@forwardimpact/libskill");
35
+ const uiLibDir = resolvePackageLib("@forwardimpact/libui");
35
36
 
36
37
  const MIME_TYPES = {
37
38
  ".html": "text/html; charset=utf-8",
@@ -138,8 +139,14 @@ export async function runDevCommand({ dataDir, options }) {
138
139
  // Serve @forwardimpact/map package files (resolved via Node module resolution)
139
140
  filePath = join(mapLibDir, pathname.slice(9));
140
141
  } else if (pathname.startsWith("/model/lib/")) {
141
- // Serve @forwardimpact/libpathway package files (resolved via Node module resolution)
142
+ // Serve @forwardimpact/libskill package files (resolved via Node module resolution)
142
143
  filePath = join(modelLibDir, pathname.slice(11));
144
+ } else if (pathname.startsWith("/ui/lib/")) {
145
+ // Serve @forwardimpact/libui package JS files
146
+ filePath = join(uiLibDir, pathname.slice(8));
147
+ } else if (pathname.startsWith("/ui/css/")) {
148
+ // Serve @forwardimpact/libui package CSS files
149
+ filePath = join(uiLibDir, "css", pathname.slice(8));
143
150
  } else if (pathname === "/" || pathname === "") {
144
151
  // Serve index.html for root
145
152
  filePath = join(publicDir, "index.html");
@@ -14,6 +14,19 @@ import { createEntityCommand } from "./command-factory.js";
14
14
  import { disciplineToMarkdown } from "../formatters/discipline/markdown.js";
15
15
  import { formatTable } from "../lib/cli-output.js";
16
16
 
17
+ /**
18
+ * Format discipline list item for --list output
19
+ * @param {Object} discipline - Discipline entity
20
+ * @returns {string} Formatted list line
21
+ */
22
+ function formatListItem(discipline) {
23
+ const type = discipline.isProfessional ? "professional" : "management";
24
+ const tracks = (discipline.validTracks || [])
25
+ .filter((t) => t !== null)
26
+ .join("|");
27
+ return `${discipline.id}, ${discipline.specialization || discipline.id}, ${type}, ${tracks || "—"}`;
28
+ }
29
+
17
30
  /**
18
31
  * Format discipline summary output
19
32
  * @param {Array} disciplines - Raw discipline entities
@@ -21,16 +34,16 @@ import { formatTable } from "../lib/cli-output.js";
21
34
  function formatSummary(disciplines) {
22
35
  console.log(`\n📋 Disciplines\n`);
23
36
 
24
- const rows = disciplines.map((d) => [
25
- d.id,
26
- d.coreSkills?.length || 0,
27
- d.supportingSkills?.length || 0,
28
- d.broadSkills?.length || 0,
29
- ]);
37
+ const rows = disciplines.map((d) => {
38
+ const type = d.isProfessional ? "Professional" : "Management";
39
+ const validTracks = (d.validTracks || []).filter((t) => t !== null);
40
+ const trackStr = validTracks.length > 0 ? validTracks.join(", ") : "—";
41
+ return [d.id, d.specialization || d.id, type, trackStr];
42
+ });
30
43
 
31
- console.log(formatTable(["ID", "Core", "Supporting", "Broad"], rows));
44
+ console.log(formatTable(["ID", "Specialization", "Type", "Tracks"], rows));
32
45
  console.log(`\nTotal: ${disciplines.length} disciplines`);
33
- console.log(`\nRun 'npx pathway discipline --list' for IDs`);
46
+ console.log(`\nRun 'npx pathway discipline --list' for IDs and names`);
34
47
  console.log(`Run 'npx pathway discipline <id>' for details\n`);
35
48
  }
36
49
 
@@ -39,8 +52,8 @@ function formatSummary(disciplines) {
39
52
  * @param {Object} viewAndContext - Contains discipline entity and context
40
53
  */
41
54
  function formatDetail(viewAndContext) {
42
- const { discipline, skills, behaviours } = viewAndContext;
43
- console.log(disciplineToMarkdown(discipline, { skills, behaviours }));
55
+ const { discipline, skills, behaviours, tracks } = viewAndContext;
56
+ console.log(disciplineToMarkdown(discipline, { skills, behaviours, tracks }));
44
57
  }
45
58
 
46
59
  export const runDisciplineCommand = createEntityCommand({
@@ -51,8 +64,10 @@ export const runDisciplineCommand = createEntityCommand({
51
64
  discipline: entity,
52
65
  skills: data.skills,
53
66
  behaviours: data.behaviours,
67
+ tracks: data.tracks,
54
68
  }),
55
69
  formatSummary,
56
70
  formatDetail,
71
+ formatListItem,
57
72
  emojiIcon: "📋",
58
73
  });
@@ -20,6 +20,15 @@ import {
20
20
  } from "../lib/cli-output.js";
21
21
  import { getConceptEmoji } from "@forwardimpact/map/levels";
22
22
 
23
+ /**
24
+ * Format driver list item for --list output
25
+ * @param {Object} driver - Driver entity
26
+ * @returns {string} Formatted list line
27
+ */
28
+ function formatListItem(driver) {
29
+ return `${driver.id}, ${driver.name}`;
30
+ }
31
+
23
32
  /**
24
33
  * Format driver summary output
25
34
  * @param {Array} drivers - Raw driver entities
@@ -43,7 +52,7 @@ function formatSummary(drivers, data) {
43
52
 
44
53
  console.log(formatTable(["ID", "Name", "Skills", "Behaviours"], rows));
45
54
  console.log(`\nTotal: ${drivers.length} drivers`);
46
- console.log(`\nRun 'npx pathway driver --list' for IDs`);
55
+ console.log(`\nRun 'npx pathway driver --list' for IDs and names`);
47
56
  console.log(`Run 'npx pathway driver <id>' for details\n`);
48
57
  }
49
58
 
@@ -90,5 +99,6 @@ export const runDriverCommand = createEntityCommand({
90
99
  }),
91
100
  formatSummary,
92
101
  formatDetail,
102
+ formatListItem,
93
103
  emojiIcon: "🎯",
94
104
  });
@@ -14,14 +14,17 @@
14
14
  * npx pathway job --validate # Validation checks
15
15
  */
16
16
 
17
- import { prepareJobDetail } from "@forwardimpact/libpathway/job";
17
+ import { prepareJobDetail } from "@forwardimpact/libskill/job";
18
18
  import { jobToMarkdown } from "../formatters/job/markdown.js";
19
- import { generateAllJobs } from "@forwardimpact/libpathway/derivation";
19
+ import {
20
+ generateJobTitle,
21
+ generateAllJobs,
22
+ } from "@forwardimpact/libskill/derivation";
20
23
  import { formatTable } from "../lib/cli-output.js";
21
24
  import {
22
25
  deriveChecklist,
23
26
  formatChecklistMarkdown,
24
- } from "@forwardimpact/libpathway/checklist";
27
+ } from "@forwardimpact/libskill/checklist";
25
28
  import { loadJobTemplate } from "../lib/template-loader.js";
26
29
  import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
27
30
 
@@ -54,13 +57,40 @@ export async function runJobCommand({ data, args, options, dataDir }) {
54
57
  validationRules: data.framework.validationRules,
55
58
  });
56
59
 
57
- // --list: Output clean lines for piping (discipline level track format)
60
+ // Apply --track filter to list and summary modes
61
+ const filteredJobs = options.track
62
+ ? jobs.filter((j) => j.track && j.track.id === options.track)
63
+ : jobs;
64
+
65
+ if (options.track && filteredJobs.length === 0 && args.length === 0) {
66
+ const trackExists = data.tracks.some((t) => t.id === options.track);
67
+ if (!trackExists) {
68
+ console.error(`Track not found: ${options.track}`);
69
+ console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
70
+ } else {
71
+ console.error(`No jobs found for track: ${options.track}`);
72
+ const trackDisciplines = data.disciplines
73
+ .filter((d) => d.validTracks && d.validTracks.includes(options.track))
74
+ .map((d) => d.id);
75
+ if (trackDisciplines.length > 0) {
76
+ console.error(
77
+ `Disciplines with this track: ${trackDisciplines.join(", ")}`,
78
+ );
79
+ }
80
+ }
81
+ process.exit(1);
82
+ }
83
+
84
+ // --list: Output descriptive comma-separated lines for piping and AI agent discovery
58
85
  if (options.list) {
59
- for (const job of jobs) {
86
+ for (const job of filteredJobs) {
87
+ const title = generateJobTitle(job.discipline, job.level, job.track);
60
88
  if (job.track) {
61
- console.log(`${job.discipline.id} ${job.level.id} ${job.track.id}`);
89
+ console.log(
90
+ `${job.discipline.id} ${job.level.id} ${job.track.id}, ${title}`,
91
+ );
62
92
  } else {
63
- console.log(`${job.discipline.id} ${job.level.id}`);
93
+ console.log(`${job.discipline.id} ${job.level.id}, ${title}`);
64
94
  }
65
95
  }
66
96
  return;
@@ -68,19 +98,40 @@ export async function runJobCommand({ data, args, options, dataDir }) {
68
98
 
69
99
  // No args: Show summary
70
100
  if (args.length === 0) {
71
- console.log(`\n💼 Jobs\n`);
101
+ const trackLabel = options.track ? ` — ${options.track}` : "";
102
+ console.log(`\n💼 Jobs${trackLabel}\n`);
72
103
 
73
- // Count by discipline
104
+ // Count by discipline with name, grouped by track
74
105
  const byDiscipline = {};
75
- for (const job of jobs) {
76
- byDiscipline[job.discipline.id] =
77
- (byDiscipline[job.discipline.id] || 0) + 1;
106
+ for (const job of filteredJobs) {
107
+ const key = job.discipline.id;
108
+ if (!byDiscipline[key]) {
109
+ byDiscipline[key] = {
110
+ name: job.discipline.specialization || job.discipline.id,
111
+ roleTitle: job.discipline.roleTitle || job.discipline.id,
112
+ type: job.discipline.isProfessional ? "Professional" : "Management",
113
+ tracks: new Set(),
114
+ count: 0,
115
+ };
116
+ }
117
+ if (job.track) byDiscipline[key].tracks.add(job.track.id);
118
+ byDiscipline[key].count++;
78
119
  }
79
120
 
80
- const rows = Object.entries(byDiscipline).map(([id, count]) => [id, count]);
81
- console.log(formatTable(["Discipline", "Combinations"], rows));
82
- console.log(`\nTotal: ${jobs.length} valid job combinations`);
83
- console.log(`\nRun 'npx pathway job --list' for all combinations`);
121
+ const rows = Object.entries(byDiscipline).map(([id, info]) => [
122
+ id,
123
+ info.name,
124
+ info.type,
125
+ info.count,
126
+ info.tracks.size > 0 ? [...info.tracks].join(", ") : "—",
127
+ ]);
128
+ console.log(
129
+ formatTable(["ID", "Specialization", "Type", "Jobs", "Tracks"], rows),
130
+ );
131
+ console.log(`\nTotal: ${filteredJobs.length} valid job combinations`);
132
+ console.log(
133
+ `\nRun 'npx pathway job --list' for all combinations with titles`,
134
+ );
84
135
  console.log(
85
136
  `Run 'npx pathway job <discipline> <level> [--track=<track>]' for details\n`,
86
137
  );
@@ -89,14 +140,28 @@ export async function runJobCommand({ data, args, options, dataDir }) {
89
140
 
90
141
  // Handle job detail view - requires discipline and level
91
142
  if (args.length < 2) {
92
- console.error(
93
- "Usage: npx pathway job <discipline> <level> [--track=<track>]",
94
- );
95
- console.error(" npx pathway job --list");
96
- console.error("Example: npx pathway job software_engineering L4");
97
- console.error(
98
- "Example: npx pathway job software_engineering L4 --track=platform",
99
- );
143
+ // Check if the single arg is a level or track, hinting at what's missing
144
+ const arg = args[0];
145
+ const isLevel = data.levels.some((g) => g.id === arg);
146
+ const isTrack = data.tracks.some((t) => t.id === arg);
147
+ if (isLevel) {
148
+ console.error(
149
+ `Missing discipline. Usage: npx pathway job <discipline> ${arg} [--track=<track>]`,
150
+ );
151
+ console.error(
152
+ `Disciplines: ${data.disciplines.map((d) => d.id).join(", ")}`,
153
+ );
154
+ } else if (isTrack) {
155
+ console.error(`Track must be passed as a flag: --track=${arg}`);
156
+ console.error(
157
+ `Usage: npx pathway job <discipline> <level> --track=${arg}`,
158
+ );
159
+ } else {
160
+ console.error(
161
+ "Usage: npx pathway job <discipline> <level> [--track=<track>]",
162
+ );
163
+ console.error(" npx pathway job --list");
164
+ }
100
165
  process.exit(1);
101
166
  }
102
167
 
@@ -107,14 +172,36 @@ export async function runJobCommand({ data, args, options, dataDir }) {
107
172
  : null;
108
173
 
109
174
  if (!discipline) {
110
- console.error(`Discipline not found: ${args[0]}`);
111
- console.error(`Available: ${data.disciplines.map((d) => d.id).join(", ")}`);
175
+ // Check if args are swapped (level first, discipline second)
176
+ const maybeLevel = data.levels.find((g) => g.id === args[0]);
177
+ const maybeDiscipline = data.disciplines.find((d) => d.id === args[1]);
178
+ if (maybeLevel && maybeDiscipline) {
179
+ console.error(`Arguments are in the wrong order. Try:`);
180
+ console.error(
181
+ ` npx pathway job ${args[1]} ${args[0]}${options.track ? ` --track=${options.track}` : ""}`,
182
+ );
183
+ } else {
184
+ console.error(`Discipline not found: ${args[0]}`);
185
+ console.error(
186
+ `Available: ${data.disciplines.map((d) => d.id).join(", ")}`,
187
+ );
188
+ }
112
189
  process.exit(1);
113
190
  }
114
191
 
115
192
  if (!level) {
116
- console.error(`Level not found: ${args[1]}`);
117
- console.error(`Available: ${data.levels.map((g) => g.id).join(", ")}`);
193
+ // Check if the second arg is a track ID passed as positional
194
+ const isTrack = data.tracks.some((t) => t.id === args[1]);
195
+ if (isTrack) {
196
+ console.error(
197
+ `Track must be passed as a flag, not a positional argument:`,
198
+ );
199
+ console.error(` npx pathway job ${args[0]} <level> --track=${args[1]}`);
200
+ console.error(`Levels: ${data.levels.map((g) => g.id).join(", ")}`);
201
+ } else {
202
+ console.error(`Level not found: ${args[1]}`);
203
+ console.error(`Available: ${data.levels.map((g) => g.id).join(", ")}`);
204
+ }
118
205
  process.exit(1);
119
206
  }
120
207
 
@@ -151,6 +238,18 @@ export async function runJobCommand({ data, args, options, dataDir }) {
151
238
  console.error(`${discipline.id} does not support tracks`);
152
239
  }
153
240
  }
241
+ // Check if it's a minLevel issue
242
+ if (discipline.minLevel) {
243
+ const levelIndex = data.levels.findIndex((g) => g.id === level.id);
244
+ const minIndex = data.levels.findIndex(
245
+ (g) => g.id === discipline.minLevel,
246
+ );
247
+ if (levelIndex >= 0 && minIndex >= 0 && levelIndex < minIndex) {
248
+ console.error(
249
+ `${discipline.id} requires minimum level: ${discipline.minLevel}`,
250
+ );
251
+ }
252
+ }
154
253
  process.exit(1);
155
254
  }
156
255
 
@@ -16,6 +16,15 @@ import { formatTable } from "../lib/cli-output.js";
16
16
  import { getConceptEmoji } from "@forwardimpact/map/levels";
17
17
  import { capitalize } from "../formatters/shared.js";
18
18
 
19
+ /**
20
+ * Format level list item for --list output
21
+ * @param {Object} level - Level entity
22
+ * @returns {string} Formatted list line
23
+ */
24
+ function formatListItem(level) {
25
+ return `${level.id}, ${level.professionalTitle || level.id}, ${level.managementTitle || level.id}`;
26
+ }
27
+
19
28
  /**
20
29
  * Format level summary output
21
30
  * @param {Array} levels - Raw level entities
@@ -29,14 +38,26 @@ function formatSummary(levels, data) {
29
38
 
30
39
  const rows = levels.map((g) => [
31
40
  g.id,
32
- g.displayName || g.id,
41
+ g.professionalTitle || g.displayName || g.id,
42
+ g.managementTitle || "-",
33
43
  g.typicalExperienceRange || "-",
34
44
  capitalize(g.baseSkillProficiencies?.primary || "-"),
35
45
  ]);
36
46
 
37
- console.log(formatTable(["ID", "Name", "Experience", "Primary Level"], rows));
47
+ console.log(
48
+ formatTable(
49
+ [
50
+ "ID",
51
+ "Professional Title",
52
+ "Management Title",
53
+ "Experience",
54
+ "Primary Level",
55
+ ],
56
+ rows,
57
+ ),
58
+ );
38
59
  console.log(`\nTotal: ${levels.length} levels`);
39
- console.log(`\nRun 'npx pathway level --list' for IDs`);
60
+ console.log(`\nRun 'npx pathway level --list' for IDs and titles`);
40
61
  console.log(`Run 'npx pathway level <id>' for details\n`);
41
62
  }
42
63
 
@@ -56,5 +77,6 @@ export const runLevelCommand = createEntityCommand({
56
77
  presentDetail: (entity) => entity,
57
78
  formatSummary,
58
79
  formatDetail,
80
+ formatListItem,
59
81
  emojiIcon: "📊",
60
82
  });
@@ -16,7 +16,7 @@ import { skillToMarkdown } from "../formatters/skill/markdown.js";
16
16
  import { prepareSkillsList } from "../formatters/skill/shared.js";
17
17
  import { getConceptEmoji } from "@forwardimpact/map/levels";
18
18
  import { formatTable, formatError } from "../lib/cli-output.js";
19
- import { generateSkillMarkdown } from "@forwardimpact/libpathway/agent";
19
+ import { generateSkillMarkdown } from "@forwardimpact/libskill/agent";
20
20
  import { formatAgentSkill } from "../formatters/agent/skill.js";
21
21
  import { loadSkillTemplate } from "../lib/template-loader.js";
22
22
 
@@ -85,6 +85,15 @@ async function formatAgentDetail(skill, stages, dataDir) {
85
85
  console.log(output);
86
86
  }
87
87
 
88
+ /**
89
+ * Format skill list item for --list output
90
+ * @param {Object} skill - Skill entity
91
+ * @returns {string} Formatted list line
92
+ */
93
+ function formatListItem(skill) {
94
+ return `${skill.id}, ${skill.name}, ${skill.capability || "-"}`;
95
+ }
96
+
88
97
  const baseSkillCommand = createEntityCommand({
89
98
  entityName: "skill",
90
99
  pluralName: "skills",
@@ -98,6 +107,7 @@ const baseSkillCommand = createEntityCommand({
98
107
  }),
99
108
  formatSummary,
100
109
  formatDetail,
110
+ formatListItem,
101
111
  emojiIcon: "📚",
102
112
  });
103
113
 
@@ -21,6 +21,15 @@ import {
21
21
  formatBullet,
22
22
  } from "../lib/cli-output.js";
23
23
 
24
+ /**
25
+ * Format stage list item for --list output
26
+ * @param {Object} stage - Stage entity
27
+ * @returns {string} Formatted list line
28
+ */
29
+ function formatListItem(stage) {
30
+ return `${stage.id}, ${stage.name}`;
31
+ }
32
+
24
33
  /**
25
34
  * Format stage summary output
26
35
  * @param {Array} stages - Raw stage entities
@@ -43,7 +52,7 @@ function formatSummary(stages, _data) {
43
52
 
44
53
  console.log(formatTable(["ID", "Name", "Mode", "Tools", "Handoffs"], rows));
45
54
  console.log(`\nTotal: ${stages.length} stages`);
46
- console.log(`\nRun 'npx pathway stage --list' for IDs`);
55
+ console.log(`\nRun 'npx pathway stage --list' for IDs and names`);
47
56
  console.log(`Run 'npx pathway stage <id>' for details\n`);
48
57
  }
49
58
 
@@ -115,5 +124,6 @@ export const runStageCommand = createEntityCommand({
115
124
  }),
116
125
  formatSummary,
117
126
  formatDetail,
127
+ formatListItem,
118
128
  emojiIcon: "🔄",
119
129
  });