@forwardimpact/pathway 0.23.0 → 0.23.2

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/README.md CHANGED
@@ -7,15 +7,15 @@ teams.
7
7
 
8
8
  Pathway is the primary interface for interacting with engineering competency
9
9
  data. It provides tools for browsing career paths, generating job descriptions,
10
- creating agent team profiles, and preparing interviews—all from a unified web
11
- experience and command line.
10
+ generating agent teams and skills, and preparing interviews—all from a unified
11
+ web experience and command line.
12
12
 
13
13
  ## What It Does
14
14
 
15
15
  - **Web application** — Interactive browser for jobs, skills, and career paths
16
16
  - **CLI tools** — Command-line access to all functionality
17
- - **Agent generation** — Create VS Code Custom Agent profiles (`.agent.md`)
18
- - **Skill generation** — Generate Agent Skills files (`SKILL.md`)
17
+ - **Agent teams** — Generate VS Code Custom Agent teams (`.agent.md`) and skills
18
+ (`SKILL.md`)
19
19
  - **Interview prep** — Build interview question sets by role
20
20
  - **Static site** — Export everything as a static site
21
21
 
@@ -29,24 +29,24 @@ npx fit-pathway serve
29
29
  npx fit-pathway skill --list
30
30
  npx fit-pathway job software_engineering senior --track=platform
31
31
 
32
- # Generate agent profiles
32
+ # Generate agent teams and skills
33
33
  npx fit-pathway agent software_engineering --track=platform --output=./.github/agents
34
34
  ```
35
35
 
36
36
  ## CLI Commands
37
37
 
38
- | Command | Description |
39
- | ----------- | ---------------------------- |
40
- | `serve` | Start web server |
41
- | `site` | Generate static site |
42
- | `init` | Create data directory |
43
- | `skill` | Browse skills |
44
- | `behaviour` | Browse behaviours |
45
- | `job` | Generate job definitions |
46
- | `agent` | Generate agent profiles |
47
- | `interview` | Generate interview questions |
48
- | `progress` | Analyze career progression |
49
- | `questions` | Browse interview questions |
38
+ | Command | Description |
39
+ | ----------- | ------------------------------- |
40
+ | `serve` | Start web server |
41
+ | `site` | Generate static site |
42
+ | `init` | Create data directory |
43
+ | `skill` | Browse skills |
44
+ | `behaviour` | Browse behaviours |
45
+ | `job` | Generate job definitions |
46
+ | `agent` | Generate agent teams and skills |
47
+ | `interview` | Generate interview questions |
48
+ | `progress` | Analyze career progression |
49
+ | `questions` | Browse interview questions |
50
50
 
51
51
  Use `--help` with any command for full options.
52
52
 
@@ -56,7 +56,7 @@ Use `--help` with any command for full options.
56
56
  - **Skill Browser** — View all skills with proficiency descriptions
57
57
  - **Career Progression** — Compare levels and identify growth areas
58
58
  - **Interview Prep** — Generate role-specific question sets
59
- - **Agent Preview** — Preview generated agent profiles
59
+ - **Agent Preview** — Preview generated agent teams and skills
60
60
 
61
61
  ## Package Exports
62
62
 
@@ -65,4 +65,4 @@ import { formatSkillForMarkdown } from "@forwardimpact/pathway/formatters";
65
65
  import { runCommand } from "@forwardimpact/pathway/commands";
66
66
  ```
67
67
 
68
- See the [documentation](../../docs/pathway/index.md) for usage details.
68
+ See the [documentation](../../website/docs/pathway/index.md) for usage details.
@@ -29,11 +29,14 @@
29
29
  * --help Show help
30
30
  */
31
31
 
32
- import { join, resolve } from "path";
32
+ import { join, resolve, dirname } from "path";
33
33
  import { existsSync } from "fs";
34
34
  import { homedir } from "os";
35
- import { loadAllData } from "@forwardimpact/map/loader";
35
+ import { fileURLToPath } from "url";
36
+ import { createDataLoader } from "@forwardimpact/map/loader";
37
+ import { validateAllData } from "@forwardimpact/map/validation";
36
38
  import { formatError } from "../src/lib/cli-output.js";
39
+ import { createTemplateLoader } from "@forwardimpact/libtemplate";
37
40
 
38
41
  // Import command handlers
39
42
  import { runDisciplineCommand } from "../src/commands/discipline.js";
@@ -54,6 +57,9 @@ import { runInitCommand } from "../src/commands/init.js";
54
57
  import { runBuildCommand } from "../src/commands/build.js";
55
58
  import { runUpdateCommand } from "../src/commands/update.js";
56
59
 
60
+ const __dirname = dirname(fileURLToPath(import.meta.url));
61
+ const TEMPLATE_DIR = join(__dirname, "..", "templates");
62
+
57
63
  const COMMANDS = {
58
64
  discipline: runDisciplineCommand,
59
65
  level: runLevelCommand,
@@ -117,7 +123,9 @@ Generate job definitions from discipline × level × track combinations.
117
123
 
118
124
  Usage:
119
125
  npx fit-pathway job Summary with stats
126
+ npx fit-pathway job --track=<track> Summary filtered by track
120
127
  npx fit-pathway job --list All valid combinations
128
+ npx fit-pathway job --list --track=<track> Combinations for a track
121
129
  npx fit-pathway job <discipline> <level> Detail view (trackless)
122
130
  npx fit-pathway job <d> <l> --track=<track> Detail view (with track)
123
131
  npx fit-pathway job <d> <l> --skills Plain list of skill IDs
@@ -126,15 +134,17 @@ Usage:
126
134
 
127
135
  Options:
128
136
  --track=TRACK Track specialization (e.g., platform, forward_deployed)
137
+ Also filters --list and summary modes
129
138
  --skills Output plain list of skill IDs (for piping)
130
139
  --tools Output plain list of tool names (for piping)
131
140
  --checklist=STAGE Show checklist for stage handoff (plan, code)
132
141
 
133
142
  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
143
+ npx fit-pathway job # overview of all jobs
144
+ npx fit-pathway job --track=forward_deployed # jobs on a specific track
145
+ npx fit-pathway job --list --track=forward_deployed # list for piping
146
+ npx fit-pathway job software_engineering J060 # trackless job detail
147
+ npx fit-pathway job software_engineering J060 --track=platform # with track
138
148
 
139
149
  ────────────────────────────────────────────────────────────────────────────────
140
150
  AGENT COMMAND
@@ -231,9 +241,7 @@ function parseArgs(args) {
231
241
  type: "full",
232
242
  compare: null,
233
243
  data: null,
234
- // Shared command options
235
244
  track: null,
236
- // Questions command options
237
245
  level: null,
238
246
  maturity: null,
239
247
  skill: null,
@@ -241,20 +249,15 @@ function parseArgs(args) {
241
249
  capability: null,
242
250
  format: null,
243
251
  stats: false,
244
- // Job command options
245
252
  checklist: null,
246
253
  skills: false,
247
254
  tools: false,
248
- // Agent command options
249
255
  output: null,
250
256
  stage: null,
251
257
  "all-stages": false,
252
258
  agent: false,
253
- // Serve command options
254
259
  port: null,
255
- // Init command options
256
260
  path: null,
257
- // Site command options
258
261
  clean: true,
259
262
  url: null,
260
263
  };
@@ -335,54 +338,45 @@ function printHelp() {
335
338
 
336
339
  /**
337
340
  * Resolve the data directory path.
338
- * Resolution order:
339
- * 1. --data=<path> flag (explicit override)
340
- * 2. PATHWAY_DATA environment variable
341
- * 3. ~/.fit/pathway/data/ (home directory install)
342
- * 4. ./data/ relative to current working directory
343
- * 5. ./examples/ relative to current working directory
344
- * 6. products/map/examples/ for monorepo development
345
- *
346
341
  * @param {Object} options - Parsed command options
347
342
  * @returns {string} Resolved absolute path to data directory
348
343
  */
349
344
  function resolveDataPath(options) {
350
- // 1. Explicit flag
351
345
  if (options.data) {
352
346
  return resolve(options.data);
353
347
  }
354
348
 
355
- // 2. Environment variable
356
349
  if (process.env.PATHWAY_DATA) {
357
350
  return resolve(process.env.PATHWAY_DATA);
358
351
  }
359
352
 
360
- // 3. Home directory install (~/.fit/pathway/data/)
361
353
  const homeData = join(homedir(), ".fit", "pathway", "data");
362
354
  if (existsSync(homeData)) {
363
355
  return homeData;
364
356
  }
365
357
 
366
- // 4. Current working directory ./data/
358
+ const cwdDataPathway = join(process.cwd(), "data/pathway");
359
+ if (existsSync(cwdDataPathway)) {
360
+ return cwdDataPathway;
361
+ }
362
+
363
+ const cwdExamplesPathway = join(process.cwd(), "examples/pathway");
364
+ if (existsSync(cwdExamplesPathway)) {
365
+ return cwdExamplesPathway;
366
+ }
367
+
367
368
  const cwdData = join(process.cwd(), "data");
368
369
  if (existsSync(cwdData)) {
369
370
  return cwdData;
370
371
  }
371
372
 
372
- // 5. Current working directory ./examples/
373
373
  const cwdExamples = join(process.cwd(), "examples");
374
374
  if (existsSync(cwdExamples)) {
375
375
  return cwdExamples;
376
376
  }
377
377
 
378
- // 6. Monorepo: products/map/examples/
379
- const mapExamples = join(process.cwd(), "products/map/examples");
380
- if (existsSync(mapExamples)) {
381
- return mapExamples;
382
- }
383
-
384
378
  throw new Error(
385
- "No data directory found. Create ./data/ or use --data=<path>",
379
+ "No data directory found. Create ./data/pathway/ or use --data=<path>",
386
380
  );
387
381
  }
388
382
 
@@ -398,7 +392,6 @@ async function main() {
398
392
  process.exit(0);
399
393
  }
400
394
 
401
- // No command: show help
402
395
  if (!options.command) {
403
396
  printHelp();
404
397
  process.exit(0);
@@ -406,7 +399,6 @@ async function main() {
406
399
 
407
400
  const command = options.command;
408
401
 
409
- // Handle init command (doesn't need data directory to exist)
410
402
  if (command === "init") {
411
403
  await runInitCommand({ options });
412
404
  process.exit(0);
@@ -414,20 +406,16 @@ async function main() {
414
406
 
415
407
  const dataDir = resolveDataPath(options);
416
408
 
417
- // Handle dev command (needs data directory)
418
409
  if (command === "dev") {
419
410
  await runDevCommand({ dataDir, options });
420
- // dev doesn't exit, keeps running
421
411
  return;
422
412
  }
423
413
 
424
- // Handle build command (generates static site)
425
414
  if (command === "build") {
426
415
  await runBuildCommand({ dataDir, options });
427
416
  process.exit(0);
428
417
  }
429
418
 
430
- // Handle update command (re-downloads bundle for local install)
431
419
  if (command === "update") {
432
420
  await runUpdateCommand({ dataDir, options });
433
421
  process.exit(0);
@@ -442,12 +430,20 @@ async function main() {
442
430
  }
443
431
 
444
432
  try {
445
- const data = await loadAllData(dataDir, {
446
- validate: true,
447
- throwOnError: true,
433
+ const loader = createDataLoader();
434
+ const templateLoader = createTemplateLoader(TEMPLATE_DIR);
435
+
436
+ const data = await loader.loadAllData(dataDir);
437
+ validateAllData(data);
438
+
439
+ await handler({
440
+ data,
441
+ args: options.args,
442
+ options,
443
+ dataDir,
444
+ templateLoader,
445
+ loader,
448
446
  });
449
-
450
- await handler({ data, args: options.args, options, dataDir });
451
447
  } catch (error) {
452
448
  console.error(formatError(error.message));
453
449
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
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": {
@@ -40,8 +40,9 @@
40
40
  "./commands": "./src/commands/index.js"
41
41
  },
42
42
  "dependencies": {
43
- "@forwardimpact/map": "^0.11.0",
43
+ "@forwardimpact/map": "^0.13.0",
44
44
  "@forwardimpact/libskill": "^3.0.0",
45
+ "@forwardimpact/libtemplate": "^0.2.0",
45
46
  "@forwardimpact/libui": "^1.0.0",
46
47
  "mustache": "^4.2.0",
47
48
  "simple-icons": "^16.7.0",
@@ -49,5 +50,8 @@
49
50
  },
50
51
  "engines": {
51
52
  "node": ">=18.0.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
52
56
  }
53
57
  }
@@ -27,10 +27,7 @@ import { writeFile, mkdir, readFile } from "fs/promises";
27
27
  import { join, dirname } from "path";
28
28
  import { existsSync } from "fs";
29
29
  import { stringify as stringifyYaml } from "yaml";
30
- import {
31
- loadAgentData,
32
- loadSkillsWithAgentData,
33
- } from "@forwardimpact/map/loader";
30
+ import { createDataLoader } from "@forwardimpact/map/loader";
34
31
  import {
35
32
  generateStageAgentProfile,
36
33
  validateAgentProfile,
@@ -50,12 +47,6 @@ import {
50
47
  formatReference,
51
48
  } from "../formatters/agent/skill.js";
52
49
  import { formatError, formatSuccess } from "../lib/cli-output.js";
53
- import {
54
- loadAgentTemplate,
55
- loadSkillTemplate,
56
- loadSkillInstallTemplate,
57
- loadSkillReferenceTemplate,
58
- } from "../lib/template-loader.js";
59
50
  import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
60
51
 
61
52
  /**
@@ -209,8 +200,7 @@ function listAgentCombinations(data, agentData, verbose = false) {
209
200
  if (humanDiscipline && humanTrack) {
210
201
  const abbrev = getDisciplineAbbreviation(discipline.id);
211
202
  const agentName = `${abbrev}-${toKebabCase(track.id)}`;
212
- const specName =
213
- humanDiscipline.specialization || humanDiscipline.id;
203
+ const specName = humanDiscipline.specialization || humanDiscipline.id;
214
204
  console.log(
215
205
  `${agentName} ${discipline.id} ${track.id}, ${specName} (${humanTrack.name})`,
216
206
  );
@@ -326,10 +316,18 @@ async function writeSkills(skills, baseDir, templates) {
326
316
  * @param {Object} params.options - Command options
327
317
  * @param {string} params.dataDir - Path to data directory
328
318
  */
329
- export async function runAgentCommand({ data, args, options, dataDir }) {
319
+ export async function runAgentCommand({
320
+ data,
321
+ args,
322
+ options,
323
+ dataDir,
324
+ templateLoader,
325
+ loader,
326
+ }) {
330
327
  // Load agent-specific data
331
- const agentData = await loadAgentData(dataDir);
332
- const skillsWithAgent = await loadSkillsWithAgentData(dataDir);
328
+ const dataLoader = loader || createDataLoader();
329
+ const agentData = await dataLoader.loadAgentData(dataDir);
330
+ const skillsWithAgent = await dataLoader.loadSkillsWithAgentData(dataDir);
333
331
 
334
332
  // --list: Output clean lines for piping
335
333
  if (options.list) {
@@ -501,7 +499,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
501
499
  }
502
500
 
503
501
  // Load template
504
- const agentTemplate = await loadAgentTemplate(dataDir);
502
+ const agentTemplate = templateLoader.load("agent.template.md", dataDir);
505
503
 
506
504
  // Output to console (default) or write to files (with --output)
507
505
  if (!options.output) {
@@ -576,10 +574,16 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
576
574
  }
577
575
 
578
576
  // Load templates
579
- const agentTemplate = await loadAgentTemplate(dataDir);
580
- const skillTemplate = await loadSkillTemplate(dataDir);
581
- const installTemplate = await loadSkillInstallTemplate(dataDir);
582
- const referenceTemplate = await loadSkillReferenceTemplate(dataDir);
577
+ const agentTemplate = templateLoader.load("agent.template.md", dataDir);
578
+ const skillTemplate = templateLoader.load("skill.template.md", dataDir);
579
+ const installTemplate = templateLoader.load(
580
+ "skill-install.template.sh",
581
+ dataDir,
582
+ );
583
+ const referenceTemplate = templateLoader.load(
584
+ "skill-reference.template.md",
585
+ dataDir,
586
+ );
583
587
  const skillTemplates = {
584
588
  skill: skillTemplate,
585
589
  install: installTemplate,
@@ -21,8 +21,8 @@ import { join, dirname, relative, resolve } from "path";
21
21
  import { fileURLToPath } from "url";
22
22
  import { execFileSync } from "child_process";
23
23
  import Mustache from "mustache";
24
- import { generateAllIndexes } from "@forwardimpact/map/index-generator";
25
- import { loadFrameworkConfig } from "@forwardimpact/map/loader";
24
+ import { createIndexGenerator } from "@forwardimpact/map/index-generator";
25
+ import { createDataLoader } from "@forwardimpact/map/loader";
26
26
 
27
27
  const __filename = fileURLToPath(import.meta.url);
28
28
  const __dirname = dirname(__filename);
@@ -85,7 +85,8 @@ export async function runBuildCommand({ dataDir, options }) {
85
85
  // Load framework config for display
86
86
  let framework;
87
87
  try {
88
- framework = await loadFrameworkConfig(dataDir);
88
+ const loader = createDataLoader();
89
+ framework = await loader.loadFrameworkConfig(dataDir);
89
90
  } catch {
90
91
  framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
91
92
  }
@@ -110,7 +111,8 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
110
111
 
111
112
  // Generate index files in data directory
112
113
  console.log("📇 Generating index files...");
113
- await generateAllIndexes(dataDir);
114
+ const indexGenerator = createIndexGenerator();
115
+ await indexGenerator.generateAllIndexes(dataDir);
114
116
 
115
117
  // Copy app assets
116
118
  console.log("📦 Copying application files...");
@@ -9,8 +9,8 @@ import { createServer } from "http";
9
9
  import { readFile, stat } from "fs/promises";
10
10
  import { join, extname, dirname } from "path";
11
11
  import { fileURLToPath } from "url";
12
- import { generateAllIndexes } from "@forwardimpact/map/index-generator";
13
- import { loadFrameworkConfig } from "@forwardimpact/map/loader";
12
+ import { createIndexGenerator } from "@forwardimpact/map/index-generator";
13
+ import { createDataLoader } from "@forwardimpact/map/loader";
14
14
 
15
15
  const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = dirname(__filename);
@@ -108,7 +108,8 @@ export async function runDevCommand({ dataDir, options }) {
108
108
  // Load framework config for display
109
109
  let framework;
110
110
  try {
111
- framework = await loadFrameworkConfig(dataDir);
111
+ const loader = createDataLoader();
112
+ framework = await loader.loadFrameworkConfig(dataDir);
112
113
  } catch {
113
114
  // Fallback if framework config fails
114
115
  framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
@@ -116,7 +117,8 @@ export async function runDevCommand({ dataDir, options }) {
116
117
 
117
118
  // Generate _index.yaml files before serving
118
119
  console.log("Generating index files...");
119
- await generateAllIndexes(dataDir);
120
+ const indexGenerator = createIndexGenerator();
121
+ await indexGenerator.generateAllIndexes(dataDir);
120
122
 
121
123
  const server = createServer(async (req, res) => {
122
124
  const url = new URL(req.url, `http://localhost:${port}`);
@@ -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,
@@ -10,7 +10,17 @@ import { fileURLToPath } from "url";
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = dirname(__filename);
13
- const examplesDir = join(__dirname, "..", "..", "examples");
13
+ // Prefer monorepo root examples/framework/, fall back to legacy co-located examples/
14
+ const monorepoExamplesDir = join(
15
+ __dirname,
16
+ "..",
17
+ "..",
18
+ "..",
19
+ "..",
20
+ "examples",
21
+ "framework",
22
+ );
23
+ const legacyExamplesDir = join(__dirname, "..", "..", "examples");
14
24
 
15
25
  /**
16
26
  * Run the init command
@@ -31,13 +41,20 @@ export async function runInitCommand({ options }) {
31
41
  // Directory doesn't exist, proceed
32
42
  }
33
43
 
34
- // Check if examples directory exists
44
+ // Find examples directory — monorepo root first, then legacy
45
+ let examplesDir;
35
46
  try {
36
- await access(examplesDir);
47
+ await access(monorepoExamplesDir);
48
+ examplesDir = monorepoExamplesDir;
37
49
  } catch {
38
- console.error("Error: Examples directory not found in package.");
39
- console.error("This may indicate a corrupted package installation.");
40
- process.exit(1);
50
+ try {
51
+ await access(legacyExamplesDir);
52
+ examplesDir = legacyExamplesDir;
53
+ } catch {
54
+ console.error("Error: Examples directory not found in package.");
55
+ console.error("This may indicate a corrupted package installation.");
56
+ process.exit(1);
57
+ }
41
58
  }
42
59
 
43
60
  // Copy example data
@@ -16,13 +16,15 @@
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,
23
26
  formatChecklistMarkdown,
24
27
  } from "@forwardimpact/libskill/checklist";
25
- import { loadJobTemplate } from "../lib/template-loader.js";
26
28
  import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
27
29
 
28
30
  /**
@@ -44,7 +46,13 @@ function formatJob(view, _options, entities, jobTemplate) {
44
46
  * @param {Object} params.options - Command options
45
47
  * @param {string} params.dataDir - Path to data directory
46
48
  */
47
- export async function runJobCommand({ data, args, options, dataDir }) {
49
+ export async function runJobCommand({
50
+ data,
51
+ args,
52
+ options,
53
+ dataDir,
54
+ templateLoader,
55
+ }) {
48
56
  const jobs = generateAllJobs({
49
57
  disciplines: data.disciplines,
50
58
  levels: data.levels,
@@ -54,9 +62,33 @@ export async function runJobCommand({ data, args, options, dataDir }) {
54
62
  validationRules: data.framework.validationRules,
55
63
  });
56
64
 
65
+ // Apply --track filter to list and summary modes
66
+ const filteredJobs = options.track
67
+ ? jobs.filter((j) => j.track && j.track.id === options.track)
68
+ : jobs;
69
+
70
+ if (options.track && filteredJobs.length === 0 && args.length === 0) {
71
+ const trackExists = data.tracks.some((t) => t.id === options.track);
72
+ if (!trackExists) {
73
+ console.error(`Track not found: ${options.track}`);
74
+ console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
75
+ } else {
76
+ console.error(`No jobs found for track: ${options.track}`);
77
+ const trackDisciplines = data.disciplines
78
+ .filter((d) => d.validTracks && d.validTracks.includes(options.track))
79
+ .map((d) => d.id);
80
+ if (trackDisciplines.length > 0) {
81
+ console.error(
82
+ `Disciplines with this track: ${trackDisciplines.join(", ")}`,
83
+ );
84
+ }
85
+ }
86
+ process.exit(1);
87
+ }
88
+
57
89
  // --list: Output descriptive comma-separated lines for piping and AI agent discovery
58
90
  if (options.list) {
59
- for (const job of jobs) {
91
+ for (const job of filteredJobs) {
60
92
  const title = generateJobTitle(job.discipline, job.level, job.track);
61
93
  if (job.track) {
62
94
  console.log(
@@ -71,29 +103,40 @@ export async function runJobCommand({ data, args, options, dataDir }) {
71
103
 
72
104
  // No args: Show summary
73
105
  if (args.length === 0) {
74
- console.log(`\n💼 Jobs\n`);
106
+ const trackLabel = options.track ? ` — ${options.track}` : "";
107
+ console.log(`\n💼 Jobs${trackLabel}\n`);
75
108
 
76
- // Count by discipline with name
109
+ // Count by discipline with name, grouped by track
77
110
  const byDiscipline = {};
78
- for (const job of jobs) {
111
+ for (const job of filteredJobs) {
79
112
  const key = job.discipline.id;
80
113
  if (!byDiscipline[key]) {
81
114
  byDiscipline[key] = {
82
115
  name: job.discipline.specialization || job.discipline.id,
116
+ roleTitle: job.discipline.roleTitle || job.discipline.id,
117
+ type: job.discipline.isProfessional ? "Professional" : "Management",
118
+ tracks: new Set(),
83
119
  count: 0,
84
120
  };
85
121
  }
122
+ if (job.track) byDiscipline[key].tracks.add(job.track.id);
86
123
  byDiscipline[key].count++;
87
124
  }
88
125
 
89
126
  const rows = Object.entries(byDiscipline).map(([id, info]) => [
90
127
  id,
91
128
  info.name,
129
+ info.type,
92
130
  info.count,
131
+ info.tracks.size > 0 ? [...info.tracks].join(", ") : "—",
93
132
  ]);
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`);
133
+ console.log(
134
+ formatTable(["ID", "Specialization", "Type", "Jobs", "Tracks"], rows),
135
+ );
136
+ console.log(`\nTotal: ${filteredJobs.length} valid job combinations`);
137
+ console.log(
138
+ `\nRun 'npx pathway job --list' for all combinations with titles`,
139
+ );
97
140
  console.log(
98
141
  `Run 'npx pathway job <discipline> <level> [--track=<track>]' for details\n`,
99
142
  );
@@ -102,14 +145,28 @@ export async function runJobCommand({ data, args, options, dataDir }) {
102
145
 
103
146
  // Handle job detail view - requires discipline and level
104
147
  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
- );
148
+ // Check if the single arg is a level or track, hinting at what's missing
149
+ const arg = args[0];
150
+ const isLevel = data.levels.some((g) => g.id === arg);
151
+ const isTrack = data.tracks.some((t) => t.id === arg);
152
+ if (isLevel) {
153
+ console.error(
154
+ `Missing discipline. Usage: npx pathway job <discipline> ${arg} [--track=<track>]`,
155
+ );
156
+ console.error(
157
+ `Disciplines: ${data.disciplines.map((d) => d.id).join(", ")}`,
158
+ );
159
+ } else if (isTrack) {
160
+ console.error(`Track must be passed as a flag: --track=${arg}`);
161
+ console.error(
162
+ `Usage: npx pathway job <discipline> <level> --track=${arg}`,
163
+ );
164
+ } else {
165
+ console.error(
166
+ "Usage: npx pathway job <discipline> <level> [--track=<track>]",
167
+ );
168
+ console.error(" npx pathway job --list");
169
+ }
113
170
  process.exit(1);
114
171
  }
115
172
 
@@ -120,14 +177,36 @@ export async function runJobCommand({ data, args, options, dataDir }) {
120
177
  : null;
121
178
 
122
179
  if (!discipline) {
123
- console.error(`Discipline not found: ${args[0]}`);
124
- console.error(`Available: ${data.disciplines.map((d) => d.id).join(", ")}`);
180
+ // Check if args are swapped (level first, discipline second)
181
+ const maybeLevel = data.levels.find((g) => g.id === args[0]);
182
+ const maybeDiscipline = data.disciplines.find((d) => d.id === args[1]);
183
+ if (maybeLevel && maybeDiscipline) {
184
+ console.error(`Arguments are in the wrong order. Try:`);
185
+ console.error(
186
+ ` npx pathway job ${args[1]} ${args[0]}${options.track ? ` --track=${options.track}` : ""}`,
187
+ );
188
+ } else {
189
+ console.error(`Discipline not found: ${args[0]}`);
190
+ console.error(
191
+ `Available: ${data.disciplines.map((d) => d.id).join(", ")}`,
192
+ );
193
+ }
125
194
  process.exit(1);
126
195
  }
127
196
 
128
197
  if (!level) {
129
- console.error(`Level not found: ${args[1]}`);
130
- console.error(`Available: ${data.levels.map((g) => g.id).join(", ")}`);
198
+ // Check if the second arg is a track ID passed as positional
199
+ const isTrack = data.tracks.some((t) => t.id === args[1]);
200
+ if (isTrack) {
201
+ console.error(
202
+ `Track must be passed as a flag, not a positional argument:`,
203
+ );
204
+ console.error(` npx pathway job ${args[0]} <level> --track=${args[1]}`);
205
+ console.error(`Levels: ${data.levels.map((g) => g.id).join(", ")}`);
206
+ } else {
207
+ console.error(`Level not found: ${args[1]}`);
208
+ console.error(`Available: ${data.levels.map((g) => g.id).join(", ")}`);
209
+ }
131
210
  process.exit(1);
132
211
  }
133
212
 
@@ -164,6 +243,18 @@ export async function runJobCommand({ data, args, options, dataDir }) {
164
243
  console.error(`${discipline.id} does not support tracks`);
165
244
  }
166
245
  }
246
+ // Check if it's a minLevel issue
247
+ if (discipline.minLevel) {
248
+ const levelIndex = data.levels.findIndex((g) => g.id === level.id);
249
+ const minIndex = data.levels.findIndex(
250
+ (g) => g.id === discipline.minLevel,
251
+ );
252
+ if (levelIndex >= 0 && minIndex >= 0 && levelIndex < minIndex) {
253
+ console.error(
254
+ `${discipline.id} requires minimum level: ${discipline.minLevel}`,
255
+ );
256
+ }
257
+ }
167
258
  process.exit(1);
168
259
  }
169
260
 
@@ -224,6 +315,6 @@ export async function runJobCommand({ data, args, options, dataDir }) {
224
315
  }
225
316
 
226
317
  // Load job template for description formatting
227
- const jobTemplate = await loadJobTemplate(dataDir);
318
+ const jobTemplate = templateLoader.load("job.template.md", dataDir);
228
319
  formatJob(view, options, { discipline, level, track }, jobTemplate);
229
320
  }
@@ -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
  );
@@ -18,7 +18,6 @@ import { getConceptEmoji } from "@forwardimpact/map/levels";
18
18
  import { formatTable, formatError } from "../lib/cli-output.js";
19
19
  import { generateSkillMarkdown } from "@forwardimpact/libskill/agent";
20
20
  import { formatAgentSkill } from "../formatters/agent/skill.js";
21
- import { loadSkillTemplate } from "../lib/template-loader.js";
22
21
 
23
22
  /**
24
23
  * Format skill summary output
@@ -69,7 +68,7 @@ function formatDetail(viewAndContext, framework) {
69
68
  * @param {Array} stages - All stage entities
70
69
  * @param {string} dataDir - Path to data directory for template loading
71
70
  */
72
- async function formatAgentDetail(skill, stages, dataDir) {
71
+ async function formatAgentDetail(skill, stages, templateLoader, dataDir) {
73
72
  if (!skill.agent) {
74
73
  console.error(formatError(`Skill '${skill.id}' has no agent section`));
75
74
  console.error(`\nSkills with agent support:`);
@@ -79,7 +78,7 @@ async function formatAgentDetail(skill, stages, dataDir) {
79
78
  process.exit(1);
80
79
  }
81
80
 
82
- const template = await loadSkillTemplate(dataDir);
81
+ const template = templateLoader.load("skill.template.md", dataDir);
83
82
  const skillMd = generateSkillMarkdown(skill, stages);
84
83
  const output = formatAgentSkill(skillMd, template);
85
84
  console.log(output);
@@ -119,7 +118,13 @@ const baseSkillCommand = createEntityCommand({
119
118
  * @param {Object} params.options - Command options
120
119
  * @param {string} params.dataDir - Path to data directory
121
120
  */
122
- export async function runSkillCommand({ data, args, options, dataDir }) {
121
+ export async function runSkillCommand({
122
+ data,
123
+ args,
124
+ options,
125
+ dataDir,
126
+ templateLoader,
127
+ }) {
123
128
  // Handle --agent flag for detail view
124
129
  if (options.agent && args.length > 0) {
125
130
  const [id] = args;
@@ -131,7 +136,7 @@ export async function runSkillCommand({ data, args, options, dataDir }) {
131
136
  process.exit(1);
132
137
  }
133
138
 
134
- await formatAgentDetail(skill, data.stages, dataDir);
139
+ await formatAgentDetail(skill, data.stages, templateLoader, dataDir);
135
140
  return;
136
141
  }
137
142
 
@@ -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`);
@@ -10,7 +10,7 @@ import { cp, mkdir, rm, readFile, writeFile, access } from "fs/promises";
10
10
  import { join } from "path";
11
11
  import { homedir } from "os";
12
12
  import { execFileSync, execSync } from "child_process";
13
- import { loadFrameworkConfig } from "@forwardimpact/map/loader";
13
+ import { createDataLoader } from "@forwardimpact/map/loader";
14
14
 
15
15
  const INSTALL_DIR = join(homedir(), ".fit", "pathway");
16
16
 
@@ -38,7 +38,8 @@ export async function runUpdateCommand({ dataDir: _dataDir, options }) {
38
38
  }
39
39
 
40
40
  // Load framework config to get siteUrl
41
- const framework = await loadFrameworkConfig(installDataDir);
41
+ const loader = createDataLoader();
42
+ const framework = await loader.loadFrameworkConfig(installDataDir);
42
43
  const siteUrl = options.url || framework.distribution?.siteUrl;
43
44
 
44
45
  if (!siteUrl) {
@@ -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", "");
@@ -23,14 +23,9 @@ import { trimValue, trimFields } from "../shared.js";
23
23
  * @returns {Object} Data object ready for Mustache template
24
24
  */
25
25
  function prepareJobDescriptionData({ job, discipline, level, track }) {
26
- // Build role summary from discipline - use manager version if applicable
27
- const isManagement = discipline.isManagement === true;
28
- let roleSummary =
29
- isManagement && discipline.managementRoleSummary
30
- ? discipline.managementRoleSummary
31
- : discipline.professionalRoleSummary || discipline.description;
32
- // Replace placeholders
26
+ // Build role summary from discipline
33
27
  const { roleTitle, specialization } = discipline;
28
+ let roleSummary = discipline.roleSummary || discipline.description;
34
29
  roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
35
30
  roleSummary = roleSummary.replace(/\{specialization\}/g, specialization);
36
31
 
@@ -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", "");
@@ -298,9 +298,15 @@ export async function loadAgentDataBrowser(dataDir = "./data") {
298
298
  loadDisciplinesFromDir(`${dataDir}/disciplines`),
299
299
  loadTracksFromDir(`${dataDir}/tracks`),
300
300
  loadBehavioursFromDir(`${dataDir}/behaviours`),
301
- tryLoadYamlFile(`${dataDir}/vscode-settings.yaml`),
302
- tryLoadYamlFile(`${dataDir}/devcontainer.yaml`),
303
- tryLoadYamlFile(`${dataDir}/copilot-setup-steps.yaml`),
301
+ tryLoadYamlFile(`${dataDir}/repository/vscode-settings.yaml`).then(
302
+ (r) => r ?? tryLoadYamlFile(`${dataDir}/vscode-settings.yaml`),
303
+ ),
304
+ tryLoadYamlFile(`${dataDir}/repository/devcontainer.yaml`).then(
305
+ (r) => r ?? tryLoadYamlFile(`${dataDir}/devcontainer.yaml`),
306
+ ),
307
+ tryLoadYamlFile(`${dataDir}/repository/copilot-setup-steps.yaml`).then(
308
+ (r) => r ?? tryLoadYamlFile(`${dataDir}/copilot-setup-steps.yaml`),
309
+ ),
304
310
  ]);
305
311
 
306
312
  return {
@@ -1,93 +0,0 @@
1
- /**
2
- * Template Loader
3
- *
4
- * Loads Mustache templates from the data directory with fallback to the
5
- * top-level templates directory. This allows users to customize agent
6
- * and skill templates by placing them in their data directory.
7
- *
8
- * Resolution order:
9
- * 1. {dataDir}/templates/{name} (user customization)
10
- * 2. {codebaseDir}/templates/{name} (fallback)
11
- */
12
-
13
- import { readFile } from "fs/promises";
14
- import { join, dirname } from "path";
15
- import { fileURLToPath } from "url";
16
- import { existsSync } from "fs";
17
-
18
- const __dirname = dirname(fileURLToPath(import.meta.url));
19
- const CODEBASE_TEMPLATES_DIR = join(__dirname, "..", "..", "templates");
20
-
21
- /**
22
- * Load a template file with fallback to codebase templates
23
- * @param {string} templateName - Template filename (e.g., 'agent.template.md')
24
- * @param {string} dataDir - Path to data directory
25
- * @returns {Promise<string>} Template content
26
- * @throws {Error} If template not found in either location
27
- */
28
- export async function loadTemplate(templateName, dataDir) {
29
- // Build list of paths to try
30
- const paths = [];
31
- if (dataDir) {
32
- paths.push(join(dataDir, "templates", templateName));
33
- }
34
- paths.push(join(CODEBASE_TEMPLATES_DIR, templateName));
35
-
36
- // Try each path in order
37
- for (const path of paths) {
38
- if (existsSync(path)) {
39
- return await readFile(path, "utf-8");
40
- }
41
- }
42
-
43
- // Not found
44
- throw new Error(
45
- `Template '${templateName}' not found. Checked:\n` +
46
- paths.map((p) => ` - ${p}`).join("\n"),
47
- );
48
- }
49
-
50
- /**
51
- * Load agent profile template
52
- * @param {string} dataDir - Path to data directory
53
- * @returns {Promise<string>} Agent template content
54
- */
55
- export async function loadAgentTemplate(dataDir) {
56
- return loadTemplate("agent.template.md", dataDir);
57
- }
58
-
59
- /**
60
- * Load agent skill template
61
- * @param {string} dataDir - Path to data directory
62
- * @returns {Promise<string>} Skill template content
63
- */
64
- export async function loadSkillTemplate(dataDir) {
65
- return loadTemplate("skill.template.md", dataDir);
66
- }
67
-
68
- /**
69
- * Load skill install script template
70
- * @param {string} dataDir - Path to data directory
71
- * @returns {Promise<string>} Install script template content
72
- */
73
- export async function loadSkillInstallTemplate(dataDir) {
74
- return loadTemplate("skill-install.template.sh", dataDir);
75
- }
76
-
77
- /**
78
- * Load skill reference template
79
- * @param {string} dataDir - Path to data directory
80
- * @returns {Promise<string>} Reference template content
81
- */
82
- export async function loadSkillReferenceTemplate(dataDir) {
83
- return loadTemplate("skill-reference.template.md", dataDir);
84
- }
85
-
86
- /**
87
- * Load job description template
88
- * @param {string} dataDir - Path to data directory
89
- * @returns {Promise<string>} Job template content
90
- */
91
- export async function loadJobTemplate(dataDir) {
92
- return loadTemplate("job.template.md", dataDir);
93
- }