@forwardimpact/pathway 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,10 +27,6 @@
27
27
  * --list Output IDs only (for piping)
28
28
  * --json Output as JSON
29
29
  * --help Show help
30
- *
31
- * Validation (moved to fit-schema):
32
- * npx fit-schema validate
33
- * npx fit-schema generate-index
34
30
  */
35
31
 
36
32
  import { join, resolve } from "path";
@@ -78,76 +74,142 @@ Engineering Pathway CLI
78
74
  Usage:
79
75
  npx fit-pathway <command> [options]
80
76
 
81
- Validation (use fit-schema instead):
82
- npx fit-schema validate Run full data validation
83
- npx fit-schema generate-index Generate browser index files
77
+ Global Options:
78
+ --list Output IDs only (for piping to other commands)
79
+ --json Output as JSON
80
+ --data=PATH Path to data directory (default: ./data or examples/)
81
+ --help Show this help message
82
+
83
+ ────────────────────────────────────────────────────────────────────────────────
84
+ GETTING STARTED
85
+ ────────────────────────────────────────────────────────────────────────────────
84
86
 
85
- Getting Started:
86
87
  init Create ./data/ with example data
87
88
  serve [--port=PORT] Serve web app at http://localhost:3000
88
89
  site [--output=PATH] Generate static site to ./site/
89
90
 
90
- Entity Commands (summary by default, --list for IDs, <id> for detail):
91
- discipline [<id>] Browse disciplines
92
- grade [<id>] Browse grades
93
- track [<id>] Browse tracks
94
- behaviour [<id>] Browse behaviours
95
- skill [<id>] Browse skills
96
- --agent Output as agent SKILL.md format
97
- driver [<id>] Browse drivers
98
- stage [<id>] Browse lifecycle stages
99
- tool [<name>] Browse recommended tools
100
-
101
- Composite Commands:
102
- job [<discipline> <grade>] [--track=TRACK] Generate job definition
103
- interview <discipline> <grade> [--track=TRACK] [--type=TYPE]
104
- Generate interview questions
105
- progress <discipline> <grade> [--track=TRACK] [--compare=GRADE]
106
- Show career progression
107
- questions [filters] Browse interview questions
108
- agent <discipline> [--track=<track>] Generate AI coding agent
91
+ ────────────────────────────────────────────────────────────────────────────────
92
+ ENTITY COMMANDS
93
+ ────────────────────────────────────────────────────────────────────────────────
109
94
 
110
- Global Options:
111
- --list Output IDs only (for piping to other commands)
112
- --json Output as JSON
113
- --data=PATH Path to data directory (default: ./data or examples/)
114
- --help Show this help message
95
+ All entity commands support: summary (default), --list (IDs for piping), <id> (detail)
115
96
 
116
- Questions Filters:
117
- --level=LEVEL Filter by skill level
118
- --maturity=MAT Filter by behaviour maturity
119
- --skill=ID Filter to specific skill
120
- --behaviour=ID Filter to behaviour
121
- --capability=CAP Filter by capability
122
- --stats Show detailed statistics
123
- --format=FORMAT Output format: table, yaml, json
124
-
125
- Agent Options:
126
- --track=TRACK Track for the agent (e.g., platform, forward_deployed)
127
- --output=PATH Write files to directory (without this, outputs to console)
128
- --stage=STAGE Generate specific stage agent (plan, code, review)
129
- --all-stages Generate all stage agents (default)
97
+ discipline [<id>] Browse engineering disciplines
98
+ grade [<id>] Browse career grades/levels
99
+ track [<id>] Browse track specializations
100
+ behaviour [<id>] Browse professional behaviours
101
+ driver [<id>] Browse outcome drivers
102
+ stage [<id>] Browse lifecycle stages
130
103
 
131
- Examples:
132
- npx fit-pathway skill # Summary of all skills
133
- npx fit-pathway skill --list # Skill IDs for piping
134
- npx fit-pathway skill ai_evaluation # Detail view
135
- npx fit-pathway skill architecture_design --agent # Agent SKILL.md output
104
+ skill [<id>] Browse skills
105
+ --agent Output as agent SKILL.md format
106
+
107
+ tool [<name>] Browse recommended tools (aggregated from skills)
108
+
109
+ ────────────────────────────────────────────────────────────────────────────────
110
+ JOB COMMAND
111
+ ────────────────────────────────────────────────────────────────────────────────
136
112
 
137
- npx fit-pathway tool # Summary of all tools
138
- npx fit-pathway tool --list # Tool names for piping
139
- npx fit-pathway tool DuckDB # Tool detail with skill usages
113
+ Generate job definitions from discipline × grade × track combinations.
140
114
 
141
- npx fit-pathway job # Summary of valid combinations
142
- npx fit-pathway job --list # All combinations for piping
115
+ Usage:
116
+ npx fit-pathway job Summary with stats
117
+ npx fit-pathway job --list All valid combinations
118
+ npx fit-pathway job <discipline> <grade> Detail view (trackless)
119
+ npx fit-pathway job <d> <g> --track=<track> Detail view (with track)
120
+ npx fit-pathway job <d> <g> --skills Plain list of skill IDs
121
+ npx fit-pathway job <d> <g> --tools Plain list of tool names
122
+ npx fit-pathway job <d> <g> --checklist=<stage> Show handoff checklist
123
+
124
+ Options:
125
+ --track=TRACK Track specialization (e.g., platform, forward_deployed)
126
+ --skills Output plain list of skill IDs (for piping)
127
+ --tools Output plain list of tool names (for piping)
128
+ --checklist=STAGE Show checklist for stage handoff (plan, code)
129
+
130
+ Examples:
143
131
  npx fit-pathway job software_engineering L4
144
132
  npx fit-pathway job software_engineering L4 --track=platform
145
- npx fit-pathway job se L3 --track=platform --checklist=code
133
+ npx fit-pathway job se L3 --track=platform --skills
134
+ npx fit-pathway job se L3 --track=platform --tools
135
+
136
+ ────────────────────────────────────────────────────────────────────────────────
137
+ AGENT COMMAND
138
+ ────────────────────────────────────────────────────────────────────────────────
139
+
140
+ Generate AI coding agent configurations from discipline × track × stage.
146
141
 
142
+ Usage:
143
+ npx fit-pathway agent Summary with stats
144
+ npx fit-pathway agent --list All valid combinations
145
+ npx fit-pathway agent <discipline> --track=<track> Generate all stage agents
146
+ npx fit-pathway agent <d> --track=<t> --stage=<s> Generate single stage agent
147
+ npx fit-pathway agent <d> --track=<t> --skills Plain list of skill IDs
148
+ npx fit-pathway agent <d> --track=<t> --tools Plain list of tool names
149
+
150
+ Options:
151
+ --track=TRACK Track for the agent (required for generation)
152
+ --stage=STAGE Generate specific stage agent (plan, code, review)
153
+ --output=PATH Write files to directory (without this, outputs to console)
154
+ --skills Output plain list of skill IDs (for piping)
155
+ --tools Output plain list of tool names (for piping)
156
+
157
+ Examples:
158
+ npx fit-pathway agent software_engineering --track=platform
159
+ npx fit-pathway agent software_engineering --track=platform --stage=plan
160
+ npx fit-pathway agent software_engineering --track=platform --output=./agents
161
+ npx fit-pathway agent software_engineering --track=platform --skills
162
+
163
+ ────────────────────────────────────────────────────────────────────────────────
164
+ INTERVIEW COMMAND
165
+ ────────────────────────────────────────────────────────────────────────────────
166
+
167
+ Generate interview question sets based on job requirements.
168
+
169
+ Usage:
170
+ npx fit-pathway interview <discipline> <grade>
171
+ npx fit-pathway interview <d> <g> --track=<track>
172
+ npx fit-pathway interview <d> <g> --type=<type>
173
+
174
+ Options:
175
+ --track=TRACK Track specialization
176
+ --type=TYPE Interview type: full (default), short
177
+
178
+ ────────────────────────────────────────────────────────────────────────────────
179
+ PROGRESS COMMAND
180
+ ────────────────────────────────────────────────────────────────────────────────
181
+
182
+ Analyze career progression between grades.
183
+
184
+ Usage:
185
+ npx fit-pathway progress <discipline> <grade>
186
+ npx fit-pathway progress <d> <g> --track=<track>
187
+ npx fit-pathway progress <d> <g> --compare=<to_grade>
188
+
189
+ Options:
190
+ --track=TRACK Track specialization
191
+ --compare=GRADE Compare to specific grade
192
+
193
+ ────────────────────────────────────────────────────────────────────────────────
194
+ QUESTIONS COMMAND
195
+ ────────────────────────────────────────────────────────────────────────────────
196
+
197
+ Browse and filter interview questions.
198
+
199
+ Usage:
200
+ npx fit-pathway questions
147
201
  npx fit-pathway questions --level=practitioner
202
+ npx fit-pathway questions --skill=architecture_design
148
203
  npx fit-pathway questions --stats
149
204
 
150
- npx fit-pathway agent software_engineering --track=platform --output=./agents
205
+ Options:
206
+ --level=LEVEL Filter by skill level
207
+ --maturity=MATURITY Filter by behaviour maturity
208
+ --skill=ID Filter to specific skill
209
+ --behaviour=ID Filter to specific behaviour
210
+ --capability=CAP Filter by capability
211
+ --stats Show detailed statistics
212
+ --format=FORMAT Output format: table, yaml, json
151
213
  `;
152
214
 
153
215
  /**
@@ -177,6 +239,8 @@ function parseArgs(args) {
177
239
  stats: false,
178
240
  // Job command options
179
241
  checklist: null,
242
+ skills: false,
243
+ tools: false,
180
244
  // Agent command options
181
245
  output: null,
182
246
  stage: null,
@@ -233,6 +297,10 @@ function parseArgs(args) {
233
297
  result.agent = true;
234
298
  } else if (arg.startsWith("--checklist=")) {
235
299
  result.checklist = arg.slice(12);
300
+ } else if (arg === "--skills") {
301
+ result.skills = true;
302
+ } else if (arg === "--tools") {
303
+ result.tools = true;
236
304
  } else if (arg.startsWith("--port=")) {
237
305
  result.port = parseInt(arg.slice(7), 10);
238
306
  } else if (arg.startsWith("--path=")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Career progression web app and CLI for exploring roles and generating agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -40,8 +40,8 @@
40
40
  "./commands": "./src/commands/index.js"
41
41
  },
42
42
  "dependencies": {
43
- "@forwardimpact/schema": "^0.2.0",
44
- "@forwardimpact/model": "^0.2.0",
43
+ "@forwardimpact/schema": "^0.3.0",
44
+ "@forwardimpact/model": "^0.3.0",
45
45
  "mustache": "^4.2.0",
46
46
  "simple-icons": "^16.7.0",
47
47
  "yaml": "^2.3.4"
@@ -13,6 +13,8 @@
13
13
  * npx pathway agent <discipline> [--track=<track>]
14
14
  * npx pathway agent <discipline> --track=<track> --stage=plan
15
15
  * npx pathway agent <discipline> --track=<track> --output=./agents
16
+ * npx pathway agent <discipline> [--track=<track>] --skills # Plain list of skill IDs
17
+ * npx pathway agent <discipline> [--track=<track>] --tools # Plain list of tool names
16
18
  * npx pathway agent --list
17
19
  *
18
20
  * Examples:
@@ -36,7 +38,9 @@ import {
36
38
  deriveReferenceGrade,
37
39
  deriveAgentSkills,
38
40
  generateSkillMd,
39
- } from "@forwardimpact/model/agent";
41
+ deriveToolkit,
42
+ buildAgentIndex,
43
+ } from "@forwardimpact/model";
40
44
  import { formatAgentProfile } from "../formatters/agent/profile.js";
41
45
  import { formatAgentSkill } from "../formatters/agent/skill.js";
42
46
  import { formatError, formatSuccess } from "../lib/cli-output.js";
@@ -44,6 +48,7 @@ import {
44
48
  loadAgentTemplate,
45
49
  loadSkillTemplate,
46
50
  } from "../lib/template-loader.js";
51
+ import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
47
52
 
48
53
  /**
49
54
  * Ensure directory exists for a file path
@@ -372,8 +377,47 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
372
377
  // Get reference grade for derivation
373
378
  const grade = deriveReferenceGrade(data.grades);
374
379
 
380
+ // --skills: Output plain list of skill IDs (for piping)
381
+ if (options.skills) {
382
+ const derivedSkills = deriveAgentSkills({
383
+ discipline: humanDiscipline,
384
+ track: humanTrack,
385
+ grade,
386
+ skills: skillsWithAgent,
387
+ });
388
+ for (const skill of derivedSkills) {
389
+ console.log(skill.skillId);
390
+ }
391
+ return;
392
+ }
393
+
394
+ // --tools: Output plain list of tool names (for piping)
395
+ if (options.tools) {
396
+ const derivedSkills = deriveAgentSkills({
397
+ discipline: humanDiscipline,
398
+ track: humanTrack,
399
+ grade,
400
+ skills: skillsWithAgent,
401
+ });
402
+ const toolkit = deriveToolkit({
403
+ skillMatrix: derivedSkills,
404
+ skills: skillsWithAgent,
405
+ });
406
+ console.log(toolkitToPlainList(toolkit));
407
+ return;
408
+ }
409
+
375
410
  const baseDir = options.output || ".";
376
411
 
412
+ // Build agent index for all valid combinations
413
+ const agentIndex = buildAgentIndex({
414
+ disciplines: data.disciplines,
415
+ tracks: data.tracks,
416
+ stages: data.stages,
417
+ agentDisciplines: agentData.disciplines,
418
+ agentTracks: agentData.tracks,
419
+ });
420
+
377
421
  // Common params for stage-based generation
378
422
  const stageParams = {
379
423
  discipline: humanDiscipline,
@@ -386,6 +430,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
386
430
  agentTrack,
387
431
  capabilities: data.capabilities,
388
432
  stages: data.stages,
433
+ agentIndex,
389
434
  };
390
435
 
391
436
  // Handle --stage flag for single stage agent
@@ -8,6 +8,8 @@
8
8
  * npx pathway job --list # All valid combinations (for piping)
9
9
  * npx pathway job <discipline> <grade> # Detail view (trackless)
10
10
  * npx pathway job <discipline> <grade> --track=<track> # Detail view (with track)
11
+ * npx pathway job <d> <g> [--track=<t>] --skills # Plain list of skill IDs
12
+ * npx pathway job <d> <g> [--track=<t>] --tools # Plain list of tool names
11
13
  * npx pathway job se L3 --track=platform --checklist=code # Show checklist for handoff
12
14
  * npx pathway job --validate # Validation checks
13
15
  */
@@ -21,6 +23,7 @@ import {
21
23
  formatChecklistMarkdown,
22
24
  } from "@forwardimpact/model/checklist";
23
25
  import { loadJobTemplate } from "../lib/template-loader.js";
26
+ import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
24
27
 
25
28
  /**
26
29
  * Format job output
@@ -132,10 +135,38 @@ export async function runJobCommand({ data, args, options, dataDir }) {
132
135
  });
133
136
 
134
137
  if (!view) {
135
- console.error("Failed to generate job output.");
138
+ const combo = track
139
+ ? `${discipline.id} × ${grade.id} × ${track.id}`
140
+ : `${discipline.id} × ${grade.id}`;
141
+ console.error(`Invalid combination: ${combo}`);
142
+ if (track) {
143
+ const validTracks =
144
+ discipline.validTracks?.filter((t) => t !== null) || [];
145
+ if (validTracks.length > 0) {
146
+ console.error(
147
+ `Valid tracks for ${discipline.id}: ${validTracks.join(", ")}`,
148
+ );
149
+ } else {
150
+ console.error(`${discipline.id} does not support tracks`);
151
+ }
152
+ }
136
153
  process.exit(1);
137
154
  }
138
155
 
156
+ // --skills: Output plain list of skill IDs (for piping)
157
+ if (options.skills) {
158
+ for (const skill of view.skillMatrix) {
159
+ console.log(skill.skillId);
160
+ }
161
+ return;
162
+ }
163
+
164
+ // --tools: Output plain list of tool names (for piping)
165
+ if (options.tools) {
166
+ console.log(toolkitToPlainList(view.toolkit));
167
+ return;
168
+ }
169
+
139
170
  if (options.json) {
140
171
  console.log(JSON.stringify(view, null, 2));
141
172
  return;
@@ -12,6 +12,12 @@ import Mustache from "mustache";
12
12
 
13
13
  import { trimValue, trimRequired, trimFields } from "../shared.js";
14
14
 
15
+ /**
16
+ * @typedef {Object} WorkingStyleEntry
17
+ * @property {string} title - Section title
18
+ * @property {string} content - Working style content (markdown)
19
+ */
20
+
15
21
  /**
16
22
  * Prepare agent profile data for template rendering
17
23
  * Normalizes string values by trimming trailing newlines for consistent template output.
@@ -27,30 +33,32 @@ import { trimValue, trimRequired, trimFields } from "../shared.js";
27
33
  * @param {string} params.bodyData.identity - Core identity text
28
34
  * @param {string} [params.bodyData.priority] - Priority/philosophy statement
29
35
  * @param {Array<{name: string, dirname: string, useWhen: string}>} params.bodyData.skillIndex - Skill index entries
30
- * @param {Array<{index: number, text: string}>} params.bodyData.beforeMakingChanges - Numbered steps
31
- * @param {string} [params.bodyData.delegation] - Delegation guidance
32
- * @param {string} params.bodyData.operationalContext - Operational context text
33
- * @param {string} params.bodyData.workingStyle - Working style markdown section
36
+ * @param {string} params.bodyData.roleContext - Role context text
37
+ * @param {WorkingStyleEntry[]} params.bodyData.workingStyles - Working style entries
34
38
  * @param {string} [params.bodyData.beforeHandoff] - Before handoff checklist markdown
35
39
  * @param {string[]} params.bodyData.constraints - List of constraints
40
+ * @param {Array<{id: string, name: string, description: string}>} [params.bodyData.agentIndex] - List of all available agents
41
+ * @param {boolean} [params.bodyData.hasAgentIndex] - Whether agent index is available
36
42
  * @returns {Object} Data object ready for Mustache template
37
43
  */
38
44
  function prepareAgentProfileData({ frontmatter, bodyData }) {
39
- // Trim array fields using helpers
45
+ // Trim array fields using shared helpers
40
46
  const handoffs = trimFields(frontmatter.handoffs, { prompt: "required" });
41
- const beforeMakingChanges = trimFields(bodyData.beforeMakingChanges, {
42
- text: "required",
43
- });
44
-
45
- // Trim simple string arrays
46
47
  const constraints = (bodyData.constraints || []).map((c) => trimRequired(c));
47
-
48
- // Trim skill index entries
49
- const skillIndex = (bodyData.skillIndex || []).map((s) => ({
50
- name: trimRequired(s.name),
51
- dirname: trimRequired(s.dirname),
52
- useWhen: trimRequired(s.useWhen),
53
- }));
48
+ const skillIndex = trimFields(bodyData.skillIndex, {
49
+ name: "required",
50
+ dirname: "required",
51
+ useWhen: "required",
52
+ });
53
+ const agentIndex = trimFields(bodyData.agentIndex, {
54
+ id: "required",
55
+ name: "required",
56
+ description: "required",
57
+ });
58
+ const workingStyles = trimFields(bodyData.workingStyles, {
59
+ title: "required",
60
+ content: "required",
61
+ });
54
62
 
55
63
  return {
56
64
  // Frontmatter
@@ -66,12 +74,14 @@ function prepareAgentProfileData({ frontmatter, bodyData }) {
66
74
  priority: trimValue(bodyData.priority),
67
75
  skillIndex,
68
76
  hasSkills: skillIndex.length > 0,
69
- beforeMakingChanges,
70
- delegation: trimValue(bodyData.delegation),
71
- operationalContext: trimValue(bodyData.operationalContext),
72
- workingStyle: trimValue(bodyData.workingStyle),
77
+ roleContext: trimValue(bodyData.roleContext),
78
+ workingStyles,
79
+ hasWorkingStyles: workingStyles.length > 0,
73
80
  beforeHandoff: trimValue(bodyData.beforeHandoff),
74
81
  constraints,
82
+ hasConstraints: constraints.length > 0,
83
+ agentIndex,
84
+ hasAgentIndex: agentIndex.length > 0,
75
85
  };
76
86
  }
77
87
 
@@ -90,10 +100,8 @@ function prepareAgentProfileData({ frontmatter, bodyData }) {
90
100
  * @param {string} profile.bodyData.identity - Core identity text
91
101
  * @param {string} [profile.bodyData.priority] - Priority/philosophy statement (optional)
92
102
  * @param {Array<{name: string, dirname: string, useWhen: string}>} profile.bodyData.skillIndex - Skill index entries
93
- * @param {Array<{index: number, text: string}>} profile.bodyData.beforeMakingChanges - Numbered steps
94
- * @param {string} [profile.bodyData.delegation] - Delegation guidance (optional)
95
- * @param {string} profile.bodyData.operationalContext - Operational context text
96
- * @param {string} profile.bodyData.workingStyle - Working style markdown section
103
+ * @param {string} profile.bodyData.roleContext - Role context text
104
+ * @param {WorkingStyleEntry[]} profile.bodyData.workingStyles - Working style entries
97
105
  * @param {string} [profile.bodyData.beforeHandoff] - Before handoff checklist markdown (optional)
98
106
  * @param {string[]} profile.bodyData.constraints - List of constraints
99
107
  * @param {string} template - Mustache template string
@@ -40,14 +40,24 @@ function prepareAgentSkillData({
40
40
  ready: "array",
41
41
  });
42
42
 
43
+ const descriptionLines = splitLines(frontmatter.description);
44
+ const useWhenLines = splitLines(frontmatter.useWhen);
45
+ const trimmedReference = trimValue(reference) || "";
46
+ const tools = toolReferences || [];
47
+
43
48
  return {
44
49
  name: frontmatter.name,
45
- descriptionLines: splitLines(frontmatter.description),
46
- useWhenLines: splitLines(frontmatter.useWhen),
50
+ descriptionLines,
51
+ hasDescription: descriptionLines.length > 0,
52
+ useWhenLines,
53
+ hasUseWhen: useWhenLines.length > 0,
47
54
  title,
48
55
  stages: processedStages,
49
- reference: trimValue(reference) || "",
50
- toolReferences: toolReferences || [],
56
+ hasStages: processedStages.length > 0,
57
+ reference: trimmedReference,
58
+ hasReference: !!trimmedReference,
59
+ toolReferences: tools,
60
+ hasToolReferences: tools.length > 0,
51
61
  };
52
62
  }
53
63
 
@@ -129,25 +129,38 @@ function prepareJobDescriptionData({ job, discipline, grade, track }) {
129
129
  grade.typicalExperienceRange || "",
130
130
  ) || null;
131
131
 
132
+ const responsibilities = trimFields(job.derivedResponsibilities, {
133
+ responsibility: "required",
134
+ });
135
+ const behaviours = trimFields(sortedBehaviours, {
136
+ maturityDescription: "optional",
137
+ });
138
+ const trimmedTrackRoleContext = trimValue(track?.roleContext);
139
+ const trimmedExpectationsParagraph = trimValue(expectationsParagraph);
140
+ const trimmedQualificationSummary = trimValue(qualificationSummary);
141
+
132
142
  return {
133
143
  title: job.title,
134
144
  gradeId: grade.id,
135
145
  typicalExperienceRange: grade.typicalExperienceRange,
136
146
  trackName: track?.name || null,
147
+ hasTrack: !!track,
137
148
  roleSummary: trimValue(roleSummary),
138
- trackRoleContext: trimValue(track?.roleContext),
139
- expectationsParagraph: trimValue(expectationsParagraph),
140
- responsibilities: trimFields(job.derivedResponsibilities, {
141
- responsibility: "required",
142
- }),
143
- behaviours: trimFields(sortedBehaviours, {
144
- maturityDescription: "optional",
145
- }),
149
+ trackRoleContext: trimmedTrackRoleContext,
150
+ hasTrackRoleContext: !!trimmedTrackRoleContext,
151
+ expectationsParagraph: trimmedExpectationsParagraph,
152
+ hasExpectationsParagraph: !!trimmedExpectationsParagraph,
153
+ responsibilities,
154
+ hasResponsibilities: responsibilities.length > 0,
155
+ behaviours,
156
+ hasBehaviours: behaviours.length > 0,
146
157
  skillLevels: skillLevels.map((level) => ({
147
158
  ...level,
148
159
  skills: trimFields(level.skills, { levelDescription: "optional" }),
149
160
  })),
150
- qualificationSummary: trimValue(qualificationSummary),
161
+ hasSkillLevels: skillLevels.length > 0,
162
+ qualificationSummary: trimmedQualificationSummary,
163
+ hasQualificationSummary: !!trimmedQualificationSummary,
151
164
  };
152
165
  }
153
166
 
@@ -17,6 +17,7 @@ import { createBehaviourProfile } from "../../components/behaviour-profile.js";
17
17
  import { createCodeDisplay } from "../../components/code-display.js";
18
18
  import { markdownToHtml } from "../../lib/markdown.js";
19
19
  import { formatJobDescription } from "./description.js";
20
+ import { createToolkitTable } from "../toolkit/dom.js";
20
21
 
21
22
  /**
22
23
  * Format job detail as DOM elements
@@ -77,14 +78,14 @@ export function jobToDOM(view, options = {}) {
77
78
  // Radar charts
78
79
  div(
79
80
  { className: "section auto-grid-lg" },
80
- createSkillRadar(view.skillMatrix, {
81
- title: "Skills Radar",
82
- size: 420,
83
- }),
84
81
  createBehaviourRadar(view.behaviourProfile, {
85
82
  title: "Behaviours Radar",
86
83
  size: 420,
87
84
  }),
85
+ createSkillRadar(view.skillMatrix, {
86
+ title: "Skills Radar",
87
+ size: 420,
88
+ }),
88
89
  ),
89
90
 
90
91
  // Job Description HTML (for print view)
@@ -104,21 +105,29 @@ export function jobToDOM(view, options = {}) {
104
105
  })
105
106
  : null,
106
107
 
107
- // Skill matrix, Behaviour profile, Driver coverage tables
108
+ // Behaviour profile, Skill matrix, Toolkit, Driver coverage tables
108
109
  showTables
109
110
  ? div(
110
111
  { className: "job-tables-section" },
111
- createDetailSection({
112
- title: "Skill Matrix",
113
- content: createSkillMatrix(view.skillMatrix),
114
- }),
115
-
116
112
  // Behaviour profile table
117
113
  createDetailSection({
118
114
  title: "Behaviour Profile",
119
115
  content: createBehaviourProfile(view.behaviourProfile),
120
116
  }),
121
117
 
118
+ createDetailSection({
119
+ title: "Skill Matrix",
120
+ content: createSkillMatrix(view.skillMatrix),
121
+ }),
122
+
123
+ // Toolkit (after skill matrix)
124
+ view.toolkit && view.toolkit.length > 0
125
+ ? createDetailSection({
126
+ title: "Tool Kit",
127
+ content: createToolkitTable(view.toolkit),
128
+ })
129
+ : null,
130
+
122
131
  // Driver coverage
123
132
  view.driverCoverage.length > 0
124
133
  ? createDetailSection({
@@ -10,6 +10,7 @@ import {
10
10
  import { formatLevel } from "../../lib/render.js";
11
11
  import { formatJobDescription } from "./description.js";
12
12
  import { SKILL_LEVEL_ORDER } from "@forwardimpact/schema/levels";
13
+ import { toolkitToMarkdown } from "../toolkit/markdown.js";
13
14
 
14
15
  /**
15
16
  * Format job detail as markdown
@@ -33,6 +34,15 @@ export function jobToMarkdown(view, entities = {}, jobTemplate) {
33
34
  lines.push("");
34
35
  }
35
36
 
37
+ // Behaviour Profile
38
+ lines.push("## Behaviour Profile", "");
39
+ const behaviourRows = view.behaviourProfile.map((b) => [
40
+ b.behaviourName,
41
+ formatLevel(b.maturity),
42
+ ]);
43
+ lines.push(tableToMarkdown(["Behaviour", "Maturity"], behaviourRows));
44
+ lines.push("");
45
+
36
46
  // Skill Matrix - sorted by level descending
37
47
  lines.push("## Skill Matrix", "");
38
48
  const sortedSkills = [...view.skillMatrix].sort((a, b) => {
@@ -50,14 +60,12 @@ export function jobToMarkdown(view, entities = {}, jobTemplate) {
50
60
  lines.push(tableToMarkdown(["Skill", "Level"], skillRows));
51
61
  lines.push("");
52
62
 
53
- // Behaviour Profile
54
- lines.push("## Behaviour Profile", "");
55
- const behaviourRows = view.behaviourProfile.map((b) => [
56
- b.behaviourName,
57
- formatLevel(b.maturity),
58
- ]);
59
- lines.push(tableToMarkdown(["Behaviour", "Maturity"], behaviourRows));
60
- lines.push("");
63
+ // Toolkit
64
+ if (view.toolkit && view.toolkit.length > 0) {
65
+ lines.push("## Tool Kit", "");
66
+ lines.push(toolkitToMarkdown(view.toolkit));
67
+ lines.push("");
68
+ }
61
69
 
62
70
  // Driver Coverage
63
71
  if (view.driverCoverage.length > 0) {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Toolkit formatting for DOM/web output
3
+ *
4
+ * Displays a compact toolkit table showing tools with icons and descriptions.
5
+ */
6
+
7
+ import {
8
+ div,
9
+ table,
10
+ thead,
11
+ tbody,
12
+ tr,
13
+ th,
14
+ td,
15
+ a,
16
+ span,
17
+ } from "../../lib/render.js";
18
+ import { createToolIcon } from "../../lib/card-mappers.js";
19
+
20
+ /**
21
+ * Create a toolkit table for display in job/agent detail pages
22
+ * @param {Array<{name: string, description: string, url?: string, simpleIcon?: string, skillIds: string[]}>} toolkit - Derived toolkit entries
23
+ * @returns {HTMLElement}
24
+ */
25
+ export function createToolkitTable(toolkit) {
26
+ if (!toolkit || toolkit.length === 0) {
27
+ return div({ className: "empty-state" }, "No tools in toolkit");
28
+ }
29
+
30
+ const rows = toolkit.map((tool) => {
31
+ const iconCell = tool.simpleIcon
32
+ ? td(
33
+ { className: "tool-icon-cell" },
34
+ createToolIcon(tool.simpleIcon, tool.name),
35
+ )
36
+ : td({ className: "tool-icon-cell" });
37
+
38
+ const nameContent = tool.url
39
+ ? a(
40
+ {
41
+ href: tool.url,
42
+ target: "_blank",
43
+ rel: "noopener noreferrer",
44
+ className: "tool-link",
45
+ },
46
+ tool.name,
47
+ span({ className: "external-icon" }, " ↗"),
48
+ )
49
+ : span({}, tool.name);
50
+
51
+ return tr(
52
+ {},
53
+ iconCell,
54
+ td({ className: "tool-name-cell" }, nameContent),
55
+ td({ className: "tool-description-cell" }, tool.description),
56
+ );
57
+ });
58
+
59
+ return div(
60
+ { className: "table-container" },
61
+ table(
62
+ { className: "table toolkit-table" },
63
+ thead(
64
+ {},
65
+ tr(
66
+ {},
67
+ th({ style: "width: 40px" }, ""),
68
+ th({}, "Tool"),
69
+ th({}, "Description"),
70
+ ),
71
+ ),
72
+ tbody({}, ...rows),
73
+ ),
74
+ );
75
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Toolkit formatting for markdown/CLI output
3
+ *
4
+ * Displays toolkit as a markdown table with tools, icons, and descriptions.
5
+ */
6
+
7
+ import { tableToMarkdown } from "../shared.js";
8
+
9
+ /**
10
+ * Format toolkit as markdown table
11
+ * @param {Array<{name: string, description: string, url?: string, simpleIcon?: string, skillIds: string[]}>} toolkit - Derived toolkit entries
12
+ * @returns {string}
13
+ */
14
+ export function toolkitToMarkdown(toolkit) {
15
+ if (!toolkit || toolkit.length === 0) {
16
+ return "";
17
+ }
18
+
19
+ const rows = toolkit.map((tool) => {
20
+ const name = tool.url ? `[${tool.name}](${tool.url})` : tool.name;
21
+ return [name, tool.description];
22
+ });
23
+
24
+ return tableToMarkdown(["Tool", "Description"], rows);
25
+ }
26
+
27
+ /**
28
+ * Format toolkit as a plain list of tool names (for --tools flag)
29
+ * @param {Array<{name: string}>} toolkit - Derived toolkit entries
30
+ * @returns {string}
31
+ */
32
+ export function toolkitToPlainList(toolkit) {
33
+ if (!toolkit || toolkit.length === 0) {
34
+ return "";
35
+ }
36
+
37
+ return toolkit.map((tool) => tool.name).join("\n");
38
+ }
@@ -27,7 +27,9 @@ import {
27
27
  generateSkillMd,
28
28
  deriveAgentSkills,
29
29
  deriveReferenceGrade,
30
- } from "@forwardimpact/model/agent";
30
+ deriveToolkit,
31
+ buildAgentIndex,
32
+ } from "@forwardimpact/model";
31
33
  import {
32
34
  createSelectWithValue,
33
35
  createDisciplineSelect,
@@ -37,6 +39,8 @@ import { getStageEmoji } from "../formatters/stage/shared.js";
37
39
  import { formatAgentProfile } from "../formatters/agent/profile.js";
38
40
  import { formatAgentSkill } from "../formatters/agent/skill.js";
39
41
  import { createCodeDisplay } from "../components/code-display.js";
42
+ import { createToolkitTable } from "../formatters/toolkit/dom.js";
43
+ import { createDetailSection } from "../components/detail.js";
40
44
 
41
45
  /** All stages option value */
42
46
  const ALL_STAGES_VALUE = "all";
@@ -247,6 +251,15 @@ export async function renderAgentBuilder() {
247
251
  // Get reference grade for derivation
248
252
  const grade = deriveReferenceGrade(data.grades);
249
253
 
254
+ // Build agent index for all valid combinations
255
+ const agentIndex = buildAgentIndex({
256
+ disciplines: data.disciplines,
257
+ tracks: data.tracks,
258
+ stages,
259
+ agentDisciplines: agentData.disciplines,
260
+ agentTracks: agentData.tracks,
261
+ });
262
+
250
263
  // Build context for generation
251
264
  const context = {
252
265
  humanDiscipline,
@@ -262,6 +275,7 @@ export async function renderAgentBuilder() {
262
275
  vscodeSettings: agentData.vscodeSettings,
263
276
  devcontainer: agentData.devcontainer,
264
277
  templates,
278
+ agentIndex,
265
279
  };
266
280
 
267
281
  // Generate preview based on stage selection
@@ -439,6 +453,7 @@ function createAllStagesPreview(context) {
439
453
  vscodeSettings,
440
454
  devcontainer,
441
455
  templates,
456
+ agentIndex,
442
457
  } = context;
443
458
 
444
459
  // Generate all stage agents
@@ -469,6 +484,7 @@ function createAllStagesPreview(context) {
469
484
  agentTrack,
470
485
  capabilities,
471
486
  stages,
487
+ agentIndex,
472
488
  });
473
489
 
474
490
  return { stage, derived, profile };
@@ -488,6 +504,12 @@ function createAllStagesPreview(context) {
488
504
  .filter((skill) => skill?.agent)
489
505
  .map((skill) => generateSkillMd(skill, stages));
490
506
 
507
+ // Derive toolkit from agent skills
508
+ const toolkit = deriveToolkit({
509
+ skillMatrix: derivedSkills,
510
+ skills,
511
+ });
512
+
491
513
  return div(
492
514
  { className: "agent-deployment" },
493
515
 
@@ -533,6 +555,14 @@ function createAllStagesPreview(context) {
533
555
  ),
534
556
  ),
535
557
 
558
+ // Tool Kit section
559
+ toolkit.length > 0
560
+ ? createDetailSection({
561
+ title: `Tool Kit (${toolkit.length})`,
562
+ content: createToolkitTable(toolkit),
563
+ })
564
+ : null,
565
+
536
566
  // CLI hint
537
567
  createCliHint(humanDiscipline.id, humanTrack.id),
538
568
  );
@@ -559,6 +589,7 @@ function createSingleStagePreview(context, stage) {
559
589
  devcontainer,
560
590
  stages,
561
591
  templates,
592
+ agentIndex,
562
593
  } = context;
563
594
 
564
595
  // Derive stage agent
@@ -588,6 +619,7 @@ function createSingleStagePreview(context, stage) {
588
619
  agentTrack,
589
620
  capabilities,
590
621
  stages,
622
+ agentIndex,
591
623
  });
592
624
 
593
625
  // Get skills for this stage (using full derived skills)
@@ -603,6 +635,12 @@ function createSingleStagePreview(context, stage) {
603
635
  .filter((skill) => skill?.agent)
604
636
  .map((skill) => generateSkillMd(skill, stages));
605
637
 
638
+ // Derive toolkit from agent skills
639
+ const toolkit = deriveToolkit({
640
+ skillMatrix: derivedSkills,
641
+ skills,
642
+ });
643
+
606
644
  return div(
607
645
  { className: "agent-deployment" },
608
646
 
@@ -642,6 +680,14 @@ function createSingleStagePreview(context, stage) {
642
680
  ),
643
681
  ),
644
682
 
683
+ // Tool Kit section
684
+ toolkit.length > 0
685
+ ? createDetailSection({
686
+ title: `Tool Kit (${toolkit.length})`,
687
+ content: createToolkitTable(toolkit),
688
+ })
689
+ : null,
690
+
645
691
  // CLI hint
646
692
  createCliHint(humanDiscipline.id, humanTrack.id, stage.id),
647
693
  );
@@ -28,10 +28,27 @@ handoffs:
28
28
  ## Core Identity
29
29
 
30
30
  {{{identity}}}
31
+ {{#priority}}
31
32
 
32
- {{#priority}} {{{priority}}}
33
+ {{{priority}}}
34
+ {{/priority}}
35
+ {{#roleContext}}
33
36
 
34
- {{/priority}} {{#hasSkills}}
37
+ ## Role Context
38
+
39
+ {{{roleContext}}}
40
+ {{/roleContext}}
41
+ {{#hasWorkingStyles}}
42
+
43
+ ## Working Style
44
+ {{#workingStyles}}
45
+
46
+ ### {{title}}
47
+
48
+ {{{content}}}
49
+ {{/workingStyles}}
50
+ {{/hasWorkingStyles}}
51
+ {{#hasSkills}}
35
52
 
36
53
  ## Available Skills and Tools
37
54
 
@@ -47,26 +64,30 @@ and (3) trade-offs of the alternative.
47
64
 
48
65
  | Skill | Location | Use When |
49
66
  | ----- | -------- | -------- |
50
-
51
- {{#skillIndex}} | {{{name}}} | `.claude/skills/{{dirname}}/SKILL.md` |
52
- {{{useWhen}}} | {{/skillIndex}}
53
-
54
- {{/hasSkills}} {{#beforeMakingChanges.length}} Before making changes:
55
-
56
- {{#beforeMakingChanges}} {{index}}. {{{text}}} {{/beforeMakingChanges}}
57
- {{/beforeMakingChanges.length}}
58
-
59
- {{#delegation}}
60
-
61
- ## Delegation
62
-
63
- {{{delegation}}} {{/delegation}}
64
-
65
- ## Operational Context
66
-
67
- {{{operationalContext}}}
68
-
69
- {{{workingStyle}}} {{#beforeHandoff}}
67
+ {{#skillIndex}}
68
+ | {{{name}}} | `.claude/skills/{{dirname}}/SKILL.md` | {{{useWhen}}} |
69
+ {{/skillIndex}}
70
+ {{/hasSkills}}
71
+ {{#hasAgentIndex}}
72
+
73
+ ## Available Sub-Agents for Delegation
74
+
75
+ **IMPORTANT:** If you come across work that is not strictly within your
76
+ speciality, then you must delegate the task using the `runSubagent` tool.
77
+
78
+ You are part of a team of agents and should not carry out all work yourself.
79
+ Rely on other agents around you that have a speciality better suited for
80
+ individual tasks. If you choose to not delegate specialised work to a sub-agent,
81
+ document in your output: (1) what the specialised work is, (2) the constraint
82
+ preventing delegation and (3) trade-offs of the alternative.
83
+
84
+ | Agent Name | Speciality | Description |
85
+ | ---------- | ---------- | ----------- |
86
+ {{#agentIndex}}
87
+ | `{{id}}` | {{{name}}} | {{{description}}} |
88
+ {{/agentIndex}}
89
+ {{/hasAgentIndex}}
90
+ {{#beforeHandoff}}
70
91
 
71
92
  ## Before Handoff
72
93
 
@@ -76,7 +97,6 @@ Before offering a handoff, verify and summarize completion of these items:
76
97
 
77
98
  When verified, summarize what was accomplished then offer the handoff. If items
78
99
  are incomplete, explain what remains.
79
-
80
100
  {{/beforeHandoff}}
81
101
 
82
102
  ## Return Format
@@ -87,10 +107,10 @@ When completing work (for handoff or as a subagent), provide:
87
107
  2. **Checklist status**: Items verified from Before Handoff section
88
108
  3. **Recommendation**: Ready for next stage, or needs more work
89
109
 
90
- {{#constraints.length}}
91
-
110
+ {{#hasConstraints}}
92
111
  ## Constraints
93
112
 
94
113
  {{#constraints}}
95
-
96
- - {{{.}}} {{/constraints}} {{/constraints.length}}
114
+ - {{{.}}}
115
+ {{/constraints}}
116
+ {{/hasConstraints}}
@@ -1,41 +1,50 @@
1
1
  # {{{title}}}
2
2
 
3
3
  - **Level:** {{{gradeId}}}
4
- - **Experience:** {{{typicalExperienceRange}}} {{#trackName}}- **Track:**
5
- {{{trackName}}} {{/trackName}}
4
+ - **Experience:** {{{typicalExperienceRange}}}
5
+ {{#hasTrack}}- **Track:** {{{trackName}}}
6
+ {{/hasTrack}}
6
7
 
7
8
  ## ROLE SUMMARY
8
9
 
9
10
  {{{roleSummary}}}
11
+ {{#hasTrackRoleContext}}
10
12
 
11
- {{#trackRoleContext}} {{{trackRoleContext}}}
13
+ {{{trackRoleContext}}}
14
+ {{/hasTrackRoleContext}}
15
+ {{#hasExpectationsParagraph}}
12
16
 
13
- {{/trackRoleContext}} {{#expectationsParagraph}} {{{expectationsParagraph}}}
14
-
15
- {{/expectationsParagraph}}
17
+ {{{expectationsParagraph}}}
18
+ {{/hasExpectationsParagraph}}
19
+ {{#hasResponsibilities}}
16
20
 
17
21
  ## ROLE RESPONSIBILITIES
18
22
 
19
23
  {{#responsibilities}}
20
-
21
- - **{{{capabilityName}}}:** {{{responsibility}}} {{/responsibilities}}
24
+ - **{{{capabilityName}}}:** {{{responsibility}}}
25
+ {{/responsibilities}}
26
+ {{/hasResponsibilities}}
27
+ {{#hasBehaviours}}
22
28
 
23
29
  ## ROLE BEHAVIOURS
24
30
 
25
31
  {{#behaviours}}
26
-
27
- - **{{{behaviourName}}}:** {{{maturityDescription}}} {{/behaviours}}
28
-
32
+ - **{{{behaviourName}}}:** {{{maturityDescription}}}
33
+ {{/behaviours}}
34
+ {{/hasBehaviours}}
35
+ {{#hasSkillLevels}}
29
36
  {{#skillLevels}}
30
37
 
31
38
  ## {{{levelHeading}}}
32
39
 
33
40
  {{#skills}}
34
-
35
- - **{{{skillName}}}:** {{{levelDescription}}} {{/skills}}
36
-
41
+ - **{{{skillName}}}:** {{{levelDescription}}}
42
+ {{/skills}}
37
43
  {{/skillLevels}}
44
+ {{/hasSkillLevels}}
45
+ {{#hasQualificationSummary}}
38
46
 
39
47
  ## QUALIFICATIONS
40
48
 
41
- {{#qualificationSummary}} {{{qualificationSummary}}} {{/qualificationSummary}}
49
+ {{{qualificationSummary}}}
50
+ {{/hasQualificationSummary}}
@@ -4,41 +4,50 @@ description: |
4
4
  {{#descriptionLines}}
5
5
  {{{.}}}
6
6
  {{/descriptionLines}}
7
- {{#useWhenLines.length}}
7
+ {{#hasUseWhen}}
8
8
  **Use When:** {{#useWhenLines}}{{{.}}}{{/useWhenLines}}
9
- {{/useWhenLines.length}}
9
+ {{/hasUseWhen}}
10
10
  ---
11
11
 
12
12
  # {{{title}}}
13
+ {{#hasUseWhen}}
13
14
 
14
- {{#useWhenLines.length}} **Use This Skill When:**
15
- {{#useWhenLines}}{{{.}}}{{/useWhenLines}} {{/useWhenLines.length}}
15
+ **Use This Skill When:**
16
+ {{#useWhenLines}}{{{.}}}{{/useWhenLines}}
17
+ {{/hasUseWhen}}
18
+ {{#hasStages}}
16
19
 
17
20
  ## Stage Guidance
18
-
19
21
  {{#stages}}
20
22
 
21
23
  ### {{stageName}} Stage
22
24
 
23
25
  **Focus:** {{{focus}}}
24
26
 
25
- **Activities:** {{#activities}}
26
-
27
- - {{{.}}} {{/activities}}
27
+ **Activities:**
28
+ {{#activities}}
29
+ - {{{.}}}
30
+ {{/activities}}
28
31
 
29
- **Ready for {{nextStageName}} when:** {{#ready}}
32
+ **Ready for {{nextStageName}} when:**
33
+ {{#ready}}
34
+ - [ ] {{{.}}}
35
+ {{/ready}}
36
+ {{/stages}}
37
+ {{/hasStages}}
38
+ {{#hasToolReferences}}
30
39
 
31
- - [ ] {{{.}}} {{/ready}} {{/stages}} {{#toolReferences.length}}
32
-
33
- ## Recommended Tools
40
+ # Recommended Tools
34
41
 
35
42
  | Tool | Use When |
36
43
  | ---- | -------- |
44
+ {{#toolReferences}}
45
+ | {{#url}}[{{{name}}}]({{{url}}}){{/url}}{{^url}}{{{name}}}{{/url}} | {{{useWhen}}} |
46
+ {{/toolReferences}}
47
+ {{/hasToolReferences}}
48
+ {{#hasReference}}
37
49
 
38
- {{#toolReferences}} |
39
- {{#url}}[{{{name}}}]({{{url}}}){{/url}}{{^url}}{{{name}}}{{/url}} |
40
- {{{useWhen}}} | {{/toolReferences}} {{/toolReferences.length}} {{#reference}}
41
-
42
- ## Reference
50
+ # Reference
43
51
 
44
- {{{reference}}} {{/reference}}
52
+ {{{reference}}}
53
+ {{/hasReference}}