@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.
- package/bin/fit-pathway.js +8 -4
- package/package.json +4 -1
- package/src/commands/agent.js +1 -2
- package/src/commands/discipline.js +15 -17
- package/src/commands/job.js +106 -20
- package/src/commands/level.js +7 -1
- package/src/commands/tool.js +3 -1
- package/src/commands/track.js +9 -3
- package/src/formatters/discipline/markdown.js +16 -1
- package/src/formatters/track/markdown.js +14 -0
package/bin/fit-pathway.js
CHANGED
|
@@ -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
|
|
135
|
-
npx fit-pathway job
|
|
136
|
-
npx fit-pathway job
|
|
137
|
-
npx fit-pathway job
|
|
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.
|
|
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
|
}
|
package/src/commands/agent.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
35
|
-
d.
|
|
36
|
-
|
|
37
|
-
d.
|
|
38
|
-
|
|
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,
|
package/src/commands/job.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
package/src/commands/level.js
CHANGED
|
@@ -46,7 +46,13 @@ function formatSummary(levels, data) {
|
|
|
46
46
|
|
|
47
47
|
console.log(
|
|
48
48
|
formatTable(
|
|
49
|
-
[
|
|
49
|
+
[
|
|
50
|
+
"ID",
|
|
51
|
+
"Professional Title",
|
|
52
|
+
"Management Title",
|
|
53
|
+
"Experience",
|
|
54
|
+
"Primary Level",
|
|
55
|
+
],
|
|
50
56
|
rows,
|
|
51
57
|
),
|
|
52
58
|
);
|
package/src/commands/tool.js
CHANGED
|
@@ -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(
|
|
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
|
|
package/src/commands/track.js
CHANGED
|
@@ -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
|
-
|
|
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", "");
|