@forwardimpact/pathway 0.23.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.
@@ -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.23.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": {
@@ -49,5 +49,8 @@
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=18.0.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
52
55
  }
53
56
  }
@@ -209,8 +209,7 @@ 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
- const specName =
213
- humanDiscipline.specialization || humanDiscipline.id;
212
+ const specName = humanDiscipline.specialization || humanDiscipline.id;
214
213
  console.log(
215
214
  `${agentName} ${discipline.id} ${track.id}, ${specName} (${humanTrack.name})`,
216
215
  );
@@ -20,7 +20,11 @@ import { formatTable } from "../lib/cli-output.js";
20
20
  * @returns {string} Formatted list line
21
21
  */
22
22
  function formatListItem(discipline) {
23
- return `${discipline.id}, ${discipline.specialization || discipline.id}, ${discipline.roleTitle || discipline.id}`;
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 || "—"}`;
24
28
  }
25
29
 
26
30
  /**
@@ -30,21 +34,14 @@ function formatListItem(discipline) {
30
34
  function formatSummary(disciplines) {
31
35
  console.log(`\n📋 Disciplines\n`);
32
36
 
33
- const rows = disciplines.map((d) => [
34
- d.id,
35
- d.specialization || d.id,
36
- d.roleTitle || d.id,
37
- d.coreSkills?.length || 0,
38
- d.supportingSkills?.length || 0,
39
- d.broadSkills?.length || 0,
40
- ]);
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
+ });
41
43
 
42
- console.log(
43
- formatTable(
44
- ["ID", "Specialization", "Role Title", "Core", "Supporting", "Broad"],
45
- rows,
46
- ),
47
- );
44
+ console.log(formatTable(["ID", "Specialization", "Type", "Tracks"], rows));
48
45
  console.log(`\nTotal: ${disciplines.length} disciplines`);
49
46
  console.log(`\nRun 'npx pathway discipline --list' for IDs and names`);
50
47
  console.log(`Run 'npx pathway discipline <id>' for details\n`);
@@ -55,8 +52,8 @@ function formatSummary(disciplines) {
55
52
  * @param {Object} viewAndContext - Contains discipline entity and context
56
53
  */
57
54
  function formatDetail(viewAndContext) {
58
- const { discipline, skills, behaviours } = viewAndContext;
59
- console.log(disciplineToMarkdown(discipline, { skills, behaviours }));
55
+ const { discipline, skills, behaviours, tracks } = viewAndContext;
56
+ console.log(disciplineToMarkdown(discipline, { skills, behaviours, tracks }));
60
57
  }
61
58
 
62
59
  export const runDisciplineCommand = createEntityCommand({
@@ -67,6 +64,7 @@ export const runDisciplineCommand = createEntityCommand({
67
64
  discipline: entity,
68
65
  skills: data.skills,
69
66
  behaviours: data.behaviours,
67
+ tracks: data.tracks,
70
68
  }),
71
69
  formatSummary,
72
70
  formatDetail,
@@ -16,7 +16,10 @@
16
16
 
17
17
  import { prepareJobDetail } from "@forwardimpact/libskill/job";
18
18
  import { jobToMarkdown } from "../formatters/job/markdown.js";
19
- import { generateJobTitle, generateAllJobs } from "@forwardimpact/libskill/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,
@@ -54,9 +57,33 @@ export async function runJobCommand({ data, args, options, dataDir }) {
54
57
  validationRules: data.framework.validationRules,
55
58
  });
56
59
 
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
+
57
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) {
60
87
  const title = generateJobTitle(job.discipline, job.level, job.track);
61
88
  if (job.track) {
62
89
  console.log(
@@ -71,29 +98,40 @@ export async function runJobCommand({ data, args, options, dataDir }) {
71
98
 
72
99
  // No args: Show summary
73
100
  if (args.length === 0) {
74
- console.log(`\n💼 Jobs\n`);
101
+ const trackLabel = options.track ? ` — ${options.track}` : "";
102
+ console.log(`\n💼 Jobs${trackLabel}\n`);
75
103
 
76
- // Count by discipline with name
104
+ // Count by discipline with name, grouped by track
77
105
  const byDiscipline = {};
78
- for (const job of jobs) {
106
+ for (const job of filteredJobs) {
79
107
  const key = job.discipline.id;
80
108
  if (!byDiscipline[key]) {
81
109
  byDiscipline[key] = {
82
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(),
83
114
  count: 0,
84
115
  };
85
116
  }
117
+ if (job.track) byDiscipline[key].tracks.add(job.track.id);
86
118
  byDiscipline[key].count++;
87
119
  }
88
120
 
89
121
  const rows = Object.entries(byDiscipline).map(([id, info]) => [
90
122
  id,
91
123
  info.name,
124
+ info.type,
92
125
  info.count,
126
+ info.tracks.size > 0 ? [...info.tracks].join(", ") : "—",
93
127
  ]);
94
- console.log(formatTable(["ID", "Specialization", "Combinations"], rows));
95
- console.log(`\nTotal: ${jobs.length} valid job combinations`);
96
- console.log(`\nRun 'npx pathway job --list' for all combinations with titles`);
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
+ );
97
135
  console.log(
98
136
  `Run 'npx pathway job <discipline> <level> [--track=<track>]' for details\n`,
99
137
  );
@@ -102,14 +140,28 @@ export async function runJobCommand({ data, args, options, dataDir }) {
102
140
 
103
141
  // Handle job detail view - requires discipline and level
104
142
  if (args.length < 2) {
105
- console.error(
106
- "Usage: npx pathway job <discipline> <level> [--track=<track>]",
107
- );
108
- console.error(" npx pathway job --list");
109
- console.error("Example: npx pathway job software_engineering L4");
110
- console.error(
111
- "Example: npx pathway job software_engineering L4 --track=platform",
112
- );
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
+ }
113
165
  process.exit(1);
114
166
  }
115
167
 
@@ -120,14 +172,36 @@ export async function runJobCommand({ data, args, options, dataDir }) {
120
172
  : null;
121
173
 
122
174
  if (!discipline) {
123
- console.error(`Discipline not found: ${args[0]}`);
124
- 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
+ }
125
189
  process.exit(1);
126
190
  }
127
191
 
128
192
  if (!level) {
129
- console.error(`Level not found: ${args[1]}`);
130
- 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
+ }
131
205
  process.exit(1);
132
206
  }
133
207
 
@@ -164,6 +238,18 @@ export async function runJobCommand({ data, args, options, dataDir }) {
164
238
  console.error(`${discipline.id} does not support tracks`);
165
239
  }
166
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
+ }
167
253
  process.exit(1);
168
254
  }
169
255
 
@@ -46,7 +46,13 @@ function formatSummary(levels, data) {
46
46
 
47
47
  console.log(
48
48
  formatTable(
49
- ["ID", "Professional Title", "Management Title", "Experience", "Primary Level"],
49
+ [
50
+ "ID",
51
+ "Professional Title",
52
+ "Management Title",
53
+ "Experience",
54
+ "Primary Level",
55
+ ],
50
56
  rows,
51
57
  ),
52
58
  );
@@ -88,7 +88,9 @@ function formatSummary(tools, totalCount) {
88
88
  if (sorted.length > 15) {
89
89
  console.log(`(showing top 15 by usage)`);
90
90
  }
91
- console.log(`\nRun 'npx pathway tool --list' for all tool names and descriptions`);
91
+ console.log(
92
+ `\nRun 'npx pathway tool --list' for all tool names and descriptions`,
93
+ );
92
94
  console.log(`Run 'npx pathway tool <name>' for details\n`);
93
95
  }
94
96
 
@@ -31,17 +31,23 @@ function formatListItem(track) {
31
31
  * @param {Object} data - Full data context
32
32
  */
33
33
  function formatSummary(tracks, data) {
34
- const { framework } = data;
34
+ const { framework, disciplines } = data;
35
35
  const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
36
36
 
37
37
  console.log(`\n${emoji} Tracks\n`);
38
38
 
39
39
  const rows = tracks.map((t) => {
40
40
  const modCount = Object.keys(t.skillModifiers || {}).length;
41
- return [t.id, t.name, modCount];
41
+ const usedBy = disciplines
42
+ ? disciplines.filter((d) => d.validTracks && d.validTracks.includes(t.id))
43
+ : [];
44
+ const disciplineNames = usedBy
45
+ .map((d) => d.specialization || d.id)
46
+ .join(", ");
47
+ return [t.id, t.name, modCount, disciplineNames || "—"];
42
48
  });
43
49
 
44
- console.log(formatTable(["ID", "Name", "Modifiers"], rows));
50
+ console.log(formatTable(["ID", "Name", "Modifiers", "Disciplines"], rows));
45
51
  console.log(`\nTotal: ${tracks.length} tracks`);
46
52
  console.log(`\nRun 'npx pathway track --list' for IDs and names`);
47
53
  console.log(`Run 'npx pathway track <id>' for details\n`);
@@ -35,16 +35,31 @@ export function disciplineListToMarkdown(disciplines) {
35
35
  * @param {Object} context - Additional context
36
36
  * @param {Array} context.skills - All skills
37
37
  * @param {Array} context.behaviours - All behaviours
38
+ * @param {Array} [context.tracks] - All tracks (for showing valid track names)
38
39
  * @param {boolean} [context.showBehaviourModifiers=true] - Whether to show behaviour modifiers section
39
40
  * @returns {string}
40
41
  */
41
42
  export function disciplineToMarkdown(
42
43
  discipline,
43
- { skills, behaviours, showBehaviourModifiers = true } = {},
44
+ { skills, behaviours, tracks, showBehaviourModifiers = true } = {},
44
45
  ) {
45
46
  const view = prepareDisciplineDetail(discipline, { skills, behaviours });
47
+ const type = discipline.isProfessional ? "Professional" : "Management";
46
48
  const lines = [`# 📋 ${view.name}`, "", view.description, ""];
47
49
 
50
+ // Type and valid tracks
51
+ const validTracks = (discipline.validTracks || []).filter((t) => t !== null);
52
+ if (validTracks.length > 0) {
53
+ const trackNames = validTracks.map((tid) => {
54
+ const track = tracks?.find((t) => t.id === tid);
55
+ return track ? track.name : tid;
56
+ });
57
+ lines.push(`**Type:** ${type} `);
58
+ lines.push(`**Valid Tracks:** ${trackNames.join(", ")}`, "");
59
+ } else {
60
+ lines.push(`**Type:** ${type}`, "");
61
+ }
62
+
48
63
  // Core skills
49
64
  if (view.coreSkills.length > 0) {
50
65
  lines.push("## Core Skills", "");
@@ -44,6 +44,20 @@ export function trackToMarkdown(
44
44
  const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
45
45
  const lines = [`# ${emoji} ${view.name}`, "", view.description, ""];
46
46
 
47
+ // Show which disciplines use this track
48
+ if (disciplines) {
49
+ const usedBy = disciplines.filter(
50
+ (d) => d.validTracks && d.validTracks.includes(track.id),
51
+ );
52
+ if (usedBy.length > 0) {
53
+ const names = usedBy.map(
54
+ (d) =>
55
+ `${d.specialization || d.id} (${d.isProfessional ? "professional" : "management"})`,
56
+ );
57
+ lines.push(`**Used by:** ${names.join(", ")}`, "");
58
+ }
59
+ }
60
+
47
61
  // Skill modifiers - show expanded skills for capabilities
48
62
  if (view.skillModifiers.length > 0) {
49
63
  lines.push("## Skill Modifiers", "");