@forwardimpact/pathway 0.2.0 → 0.4.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.
- package/app/commands/agent.js +20 -20
- package/app/commands/index.js +4 -3
- package/app/commands/job.js +9 -4
- package/app/commands/skill.js +56 -2
- package/app/commands/tool.js +112 -0
- package/app/components/builder.js +6 -3
- package/app/components/checklist.js +6 -4
- package/app/components/markdown-textarea.js +132 -0
- package/app/css/components/forms.css +45 -0
- package/app/css/components/layout.css +12 -0
- package/app/css/components/surfaces.css +22 -0
- package/app/css/pages/detail.css +50 -0
- package/app/css/pages/job-builder.css +0 -42
- package/app/formatters/agent/profile.js +61 -120
- package/app/formatters/agent/skill.js +48 -60
- package/app/formatters/grade/dom.js +2 -4
- package/app/formatters/job/description.js +74 -82
- package/app/formatters/job/dom.js +45 -179
- package/app/formatters/job/markdown.js +17 -13
- package/app/formatters/shared.js +65 -2
- package/app/formatters/skill/dom.js +57 -2
- package/app/formatters/skill/markdown.js +18 -0
- package/app/formatters/skill/shared.js +12 -4
- package/app/formatters/stage/microdata.js +1 -1
- package/app/formatters/stage/shared.js +1 -1
- package/app/formatters/tool/shared.js +72 -0
- package/app/handout-main.js +7 -7
- package/app/handout.html +7 -0
- package/app/index.html +10 -3
- package/app/lib/card-mappers.js +64 -17
- package/app/lib/form-controls.js +64 -1
- package/app/lib/render.js +12 -1
- package/app/lib/template-loader.js +9 -0
- package/app/lib/yaml-loader.js +12 -1
- package/app/main.js +4 -0
- package/app/model/agent.js +26 -18
- package/app/model/derivation.js +3 -3
- package/app/model/levels.js +2 -0
- package/app/model/loader.js +12 -1
- package/app/model/validation.js +74 -8
- package/app/pages/agent-builder.js +8 -5
- package/app/pages/job.js +28 -4
- package/app/pages/landing.js +34 -14
- package/app/pages/progress.js +6 -5
- package/app/pages/self-assessment.js +10 -8
- package/app/pages/skill.js +5 -17
- package/app/pages/stage.js +10 -6
- package/app/pages/tool.js +50 -0
- package/app/slides/index.js +25 -25
- package/app/slides.html +7 -0
- package/bin/pathway.js +41 -27
- package/examples/capabilities/business.yaml +17 -17
- package/examples/capabilities/delivery.yaml +51 -36
- package/examples/capabilities/reliability.yaml +127 -114
- package/examples/capabilities/scale.yaml +38 -36
- package/examples/disciplines/engineering_management.yaml +1 -1
- package/examples/framework.yaml +12 -0
- package/examples/grades.yaml +18 -19
- package/examples/self-assessments.yaml +1 -1
- package/package.json +1 -1
- package/templates/job.template.md +47 -0
- package/templates/skill.template.md +31 -12
package/app/handout-main.js
CHANGED
|
@@ -135,25 +135,25 @@ function renderIndex(data) {
|
|
|
135
135
|
`${getConceptEmoji(framework, "job")} ${framework.entityDefinitions.job.title}`,
|
|
136
136
|
),
|
|
137
137
|
" - ",
|
|
138
|
-
`${data.disciplines.length} disciplines, ${data.
|
|
138
|
+
`${data.disciplines.length} disciplines, ${data.grades.length} grades, ${data.tracks.length} tracks`,
|
|
139
139
|
),
|
|
140
140
|
li(
|
|
141
141
|
{},
|
|
142
142
|
a(
|
|
143
|
-
{ href: "#/
|
|
144
|
-
`${getConceptEmoji(framework, "
|
|
143
|
+
{ href: "#/behaviour" },
|
|
144
|
+
`${getConceptEmoji(framework, "behaviour")} ${framework.entityDefinitions.behaviour.title}`,
|
|
145
145
|
),
|
|
146
146
|
" - ",
|
|
147
|
-
`${data.
|
|
147
|
+
`${data.behaviours.length} behaviour definitions`,
|
|
148
148
|
),
|
|
149
149
|
li(
|
|
150
150
|
{},
|
|
151
151
|
a(
|
|
152
|
-
{ href: "#/
|
|
153
|
-
`${getConceptEmoji(framework, "
|
|
152
|
+
{ href: "#/skill" },
|
|
153
|
+
`${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
|
|
154
154
|
),
|
|
155
155
|
" - ",
|
|
156
|
-
`${data.
|
|
156
|
+
`${data.skills.length} skill definitions`,
|
|
157
157
|
),
|
|
158
158
|
li(
|
|
159
159
|
{},
|
package/app/handout.html
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Engineering Pathway - Handout View</title>
|
|
7
7
|
<link rel="stylesheet" href="css/bundles/handout.css" />
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"mustache": "https://esm.sh/mustache@4.2.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
8
15
|
</head>
|
|
9
16
|
<body class="slide-view handout-view">
|
|
10
17
|
<header
|
package/app/index.html
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Engineering Pathway</title>
|
|
7
7
|
<link rel="stylesheet" href="css/bundles/app.css" />
|
|
8
|
+
<link
|
|
9
|
+
rel="stylesheet"
|
|
10
|
+
href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"
|
|
11
|
+
/>
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markdown.min.js"></script>
|
|
8
14
|
<script type="importmap">
|
|
9
15
|
{
|
|
10
16
|
"imports": {
|
|
@@ -31,12 +37,13 @@
|
|
|
31
37
|
</button>
|
|
32
38
|
<ul class="nav-links" id="nav-links">
|
|
33
39
|
<li><a href="#/discipline">Disciplines</a></li>
|
|
34
|
-
<li><a href="#/track">Tracks</a></li>
|
|
35
40
|
<li><a href="#/grade">Grades</a></li>
|
|
36
|
-
<li><a href="#/
|
|
41
|
+
<li><a href="#/track">Tracks</a></li>
|
|
37
42
|
<li><a href="#/behaviour">Behaviours</a></li>
|
|
38
|
-
<li><a href="#/
|
|
43
|
+
<li><a href="#/skill">Skills</a></li>
|
|
39
44
|
<li><a href="#/driver">Drivers</a></li>
|
|
45
|
+
<li><a href="#/stage">Stages</a></li>
|
|
46
|
+
<li><a href="#/tool">Tools</a></li>
|
|
40
47
|
<li><a href="#/job-builder" class="nav-cta">Build a Job</a></li>
|
|
41
48
|
<li><a href="#/agent-builder" class="nav-cta">Build an Agent</a></li>
|
|
42
49
|
<li><a href="#/interview-prep" class="nav-cta">Interview Prep</a></li>
|
package/app/lib/card-mappers.js
CHANGED
|
@@ -9,6 +9,23 @@ import { createBadge } from "../components/card.js";
|
|
|
9
9
|
import { formatLevel } from "./render.js";
|
|
10
10
|
import { getCapabilityEmoji } from "../model/levels.js";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Create an external link element styled as a badge
|
|
14
|
+
* @param {string} text - Link text
|
|
15
|
+
* @param {string} url - External URL
|
|
16
|
+
* @returns {HTMLElement}
|
|
17
|
+
*/
|
|
18
|
+
function createExternalLink(text, url) {
|
|
19
|
+
const link = document.createElement("a");
|
|
20
|
+
link.href = url;
|
|
21
|
+
link.target = "_blank";
|
|
22
|
+
link.rel = "noopener noreferrer";
|
|
23
|
+
link.className = "badge badge-primary";
|
|
24
|
+
link.textContent = text;
|
|
25
|
+
link.addEventListener("click", (e) => e.stopPropagation()); // Don't trigger card click
|
|
26
|
+
return link;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
/**
|
|
13
30
|
* Map discipline to card config
|
|
14
31
|
* @param {Object} discipline
|
|
@@ -155,24 +172,54 @@ export function jobToCardConfig(job) {
|
|
|
155
172
|
}
|
|
156
173
|
|
|
157
174
|
/**
|
|
158
|
-
*
|
|
159
|
-
* @param {
|
|
175
|
+
* Map tool to card config
|
|
176
|
+
* @param {Object} tool - Aggregated tool with usages
|
|
177
|
+
* @param {Array} capabilities - Capability entities for emoji lookup
|
|
178
|
+
* @returns {Object}
|
|
179
|
+
*/
|
|
180
|
+
export function toolToCardConfig(tool, capabilities) {
|
|
181
|
+
// Create skills list as card content
|
|
182
|
+
const skillsList = createSkillsList(tool.usages, capabilities);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
title: tool.name,
|
|
186
|
+
description: tool.description,
|
|
187
|
+
// Docs link in header badges (upper right)
|
|
188
|
+
badges: tool.url ? [createExternalLink("Docs →", tool.url)] : [],
|
|
189
|
+
content: skillsList,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create an unordered list of skill links with capability emoji
|
|
195
|
+
* @param {Array} usages - Tool usage objects with skillId, skillName, capabilityId
|
|
196
|
+
* @param {Array} capabilities - Capability entities
|
|
197
|
+
* @returns {HTMLElement}
|
|
198
|
+
*/
|
|
199
|
+
function createSkillsList(usages, capabilities) {
|
|
200
|
+
const ul = document.createElement("ul");
|
|
201
|
+
ul.className = "tool-skills-list";
|
|
202
|
+
|
|
203
|
+
for (const usage of usages) {
|
|
204
|
+
const emoji = getCapabilityEmoji(capabilities, usage.capabilityId);
|
|
205
|
+
const li = document.createElement("li");
|
|
206
|
+
const link = document.createElement("a");
|
|
207
|
+
link.href = `#/skill/${usage.skillId}`;
|
|
208
|
+
link.textContent = `${emoji} ${usage.skillName}`;
|
|
209
|
+
li.appendChild(link);
|
|
210
|
+
ul.appendChild(li);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return ul;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Format capability for badge display (short, tag-like)
|
|
218
|
+
* @param {string} capabilityId
|
|
160
219
|
* @param {Array} capabilities
|
|
161
220
|
* @returns {string}
|
|
162
221
|
*/
|
|
163
|
-
function formatCapability(
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
scale: "Scale",
|
|
167
|
-
reliability: "Reliability",
|
|
168
|
-
data: "Data",
|
|
169
|
-
ai: "AI",
|
|
170
|
-
process: "Process",
|
|
171
|
-
business: "Business",
|
|
172
|
-
people: "People",
|
|
173
|
-
documentation: "Documentation",
|
|
174
|
-
};
|
|
175
|
-
const label = capabilityLabels[capability] || formatLevel(capability);
|
|
176
|
-
const emoji = getCapabilityEmoji(capabilities, capability);
|
|
177
|
-
return `${emoji} ${label}`;
|
|
222
|
+
function formatCapability(capabilityId, capabilities) {
|
|
223
|
+
const emoji = getCapabilityEmoji(capabilities, capabilityId);
|
|
224
|
+
return `${emoji} ${capabilityId.toUpperCase()}`;
|
|
178
225
|
}
|
package/app/lib/form-controls.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Reusable form control components
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { select, option } from "./render.js";
|
|
5
|
+
import { select, option, optgroup } from "./render.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Create a select element with initial value and change handler
|
|
@@ -45,3 +45,66 @@ export function createSelectWithValue({
|
|
|
45
45
|
|
|
46
46
|
return selectEl;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a discipline select with optgroups for Professional and Management
|
|
51
|
+
* @param {Object} options - Configuration options
|
|
52
|
+
* @param {string} options.id - Element ID
|
|
53
|
+
* @param {Array} options.disciplines - Array of discipline objects
|
|
54
|
+
* @param {string} options.initialValue - Initial selected value
|
|
55
|
+
* @param {string} options.placeholder - Placeholder text for empty option
|
|
56
|
+
* @param {Function} options.onChange - Callback when selection changes
|
|
57
|
+
* @param {Function} [options.getDisplayName] - Optional function to get display name from item
|
|
58
|
+
* @returns {HTMLElement}
|
|
59
|
+
*/
|
|
60
|
+
export function createDisciplineSelect({
|
|
61
|
+
id,
|
|
62
|
+
disciplines,
|
|
63
|
+
initialValue,
|
|
64
|
+
placeholder,
|
|
65
|
+
onChange,
|
|
66
|
+
getDisplayName,
|
|
67
|
+
}) {
|
|
68
|
+
const displayFn = getDisplayName || ((d) => d.specialization || d.name);
|
|
69
|
+
|
|
70
|
+
// Separate disciplines by type
|
|
71
|
+
const professional = disciplines.filter((d) => d.isProfessional);
|
|
72
|
+
const management = disciplines.filter((d) => d.isManagement);
|
|
73
|
+
|
|
74
|
+
// Sort each group alphabetically by display name
|
|
75
|
+
professional.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
|
|
76
|
+
management.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
|
|
77
|
+
|
|
78
|
+
// Build options for a group
|
|
79
|
+
const buildOptions = (items) =>
|
|
80
|
+
items.map((item) => {
|
|
81
|
+
const opt = option({ value: item.id }, displayFn(item));
|
|
82
|
+
if (item.id === initialValue) {
|
|
83
|
+
opt.selected = true;
|
|
84
|
+
}
|
|
85
|
+
return opt;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Build optgroups - Professional first, then Management
|
|
89
|
+
const groups = [];
|
|
90
|
+
if (professional.length > 0) {
|
|
91
|
+
groups.push(
|
|
92
|
+
optgroup({ label: "Professional" }, ...buildOptions(professional)),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (management.length > 0) {
|
|
96
|
+
groups.push(optgroup({ label: "Management" }, ...buildOptions(management)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selectEl = select(
|
|
100
|
+
{ className: "form-select", id },
|
|
101
|
+
option({ value: "" }, placeholder),
|
|
102
|
+
...groups,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
selectEl.addEventListener("change", (e) => {
|
|
106
|
+
onChange(e.target.value);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return selectEl;
|
|
110
|
+
}
|
package/app/lib/render.js
CHANGED
|
@@ -111,6 +111,10 @@ export const th = (attrs, ...children) =>
|
|
|
111
111
|
createElement("th", attrs, ...children);
|
|
112
112
|
export const td = (attrs, ...children) =>
|
|
113
113
|
createElement("td", attrs, ...children);
|
|
114
|
+
export const pre = (attrs, ...children) =>
|
|
115
|
+
createElement("pre", attrs, ...children);
|
|
116
|
+
export const code = (attrs, ...children) =>
|
|
117
|
+
createElement("code", attrs, ...children);
|
|
114
118
|
export const button = (attrs, ...children) =>
|
|
115
119
|
createElement("button", attrs, ...children);
|
|
116
120
|
export const input = (attrs) => createElement("input", attrs);
|
|
@@ -118,6 +122,8 @@ export const select = (attrs, ...children) =>
|
|
|
118
122
|
createElement("select", attrs, ...children);
|
|
119
123
|
export const option = (attrs, ...children) =>
|
|
120
124
|
createElement("option", attrs, ...children);
|
|
125
|
+
export const optgroup = (attrs, ...children) =>
|
|
126
|
+
createElement("optgroup", attrs, ...children);
|
|
121
127
|
export const label = (attrs, ...children) =>
|
|
122
128
|
createElement("label", attrs, ...children);
|
|
123
129
|
export const form = (attrs, ...children) =>
|
|
@@ -185,12 +191,17 @@ export function showError(message) {
|
|
|
185
191
|
|
|
186
192
|
/**
|
|
187
193
|
* Format a skill level or behaviour maturity for display
|
|
194
|
+
* Handles both snake_case and camelCase
|
|
188
195
|
* @param {string} value - The level/maturity value
|
|
189
196
|
* @returns {string}
|
|
190
197
|
*/
|
|
191
198
|
export function formatLevel(value) {
|
|
192
199
|
if (!value) return "";
|
|
193
|
-
|
|
200
|
+
// Insert space before uppercase letters (for camelCase), then handle snake_case
|
|
201
|
+
return value
|
|
202
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
203
|
+
.replace(/_/g, " ")
|
|
204
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
194
205
|
}
|
|
195
206
|
|
|
196
207
|
/**
|
|
@@ -64,3 +64,12 @@ export async function loadAgentTemplate(dataDir) {
|
|
|
64
64
|
export async function loadSkillTemplate(dataDir) {
|
|
65
65
|
return loadTemplate("skill.template.md", dataDir);
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load job description template
|
|
70
|
+
* @param {string} dataDir - Path to data directory
|
|
71
|
+
* @returns {Promise<string>} Job template content
|
|
72
|
+
*/
|
|
73
|
+
export async function loadJobTemplate(dataDir) {
|
|
74
|
+
return loadTemplate("job.template.md", dataDir);
|
|
75
|
+
}
|
package/app/lib/yaml-loader.js
CHANGED
|
@@ -70,7 +70,15 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
70
70
|
|
|
71
71
|
if (capability.skills && Array.isArray(capability.skills)) {
|
|
72
72
|
for (const skill of capability.skills) {
|
|
73
|
-
const {
|
|
73
|
+
const {
|
|
74
|
+
id,
|
|
75
|
+
name,
|
|
76
|
+
isHumanOnly,
|
|
77
|
+
human,
|
|
78
|
+
agent,
|
|
79
|
+
implementationReference,
|
|
80
|
+
toolReferences,
|
|
81
|
+
} = skill;
|
|
74
82
|
allSkills.push({
|
|
75
83
|
id,
|
|
76
84
|
name,
|
|
@@ -80,6 +88,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
80
88
|
// Include isHumanOnly flag for agent filtering (defaults to false)
|
|
81
89
|
...(isHumanOnly && { isHumanOnly }),
|
|
82
90
|
...(agent && { agent }),
|
|
91
|
+
// Include implementation reference and tool references (shared by human and agent)
|
|
92
|
+
...(implementationReference && { implementationReference }),
|
|
93
|
+
...(toolReferences && { toolReferences }),
|
|
83
94
|
});
|
|
84
95
|
}
|
|
85
96
|
}
|
package/app/main.js
CHANGED
|
@@ -26,6 +26,7 @@ import { renderTracksList, renderTrackDetail } from "./pages/track.js";
|
|
|
26
26
|
import { renderGradesList, renderGradeDetail } from "./pages/grade.js";
|
|
27
27
|
import { renderDriversList, renderDriverDetail } from "./pages/driver.js";
|
|
28
28
|
import { renderStagesList, renderStageDetail } from "./pages/stage.js";
|
|
29
|
+
import { renderToolsList } from "./pages/tool.js";
|
|
29
30
|
import { renderJobBuilder } from "./pages/job-builder.js";
|
|
30
31
|
import { renderJobDetail } from "./pages/job.js";
|
|
31
32
|
import { renderInterviewPrep } from "./pages/interview-builder.js";
|
|
@@ -99,6 +100,9 @@ function setupRoutes() {
|
|
|
99
100
|
router.on("/stage", renderStagesList);
|
|
100
101
|
router.on("/stage/:id", renderStageDetail);
|
|
101
102
|
|
|
103
|
+
// Tool
|
|
104
|
+
router.on("/tool", renderToolsList);
|
|
105
|
+
|
|
102
106
|
// Job builder
|
|
103
107
|
router.on("/job-builder", renderJobBuilder);
|
|
104
108
|
router.on("/job/:discipline/:grade/:track", renderJobDetail);
|
package/app/model/agent.js
CHANGED
|
@@ -225,21 +225,13 @@ function buildWorkingStyleFromBehaviours(
|
|
|
225
225
|
return sections.join("\n");
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/**
|
|
229
|
-
* Stage ID to display name and next stage mapping
|
|
230
|
-
*/
|
|
231
|
-
const STAGE_INFO = {
|
|
232
|
-
plan: { name: "Plan", nextStage: "Code" },
|
|
233
|
-
code: { name: "Code", nextStage: "Review" },
|
|
234
|
-
review: { name: "Review", nextStage: "Complete" },
|
|
235
|
-
};
|
|
236
|
-
|
|
237
228
|
/**
|
|
238
229
|
* Generate SKILL.md content from skill data
|
|
239
230
|
* @param {Object} skillData - Skill with agent section containing stages
|
|
231
|
+
* @param {Array} stages - All stage entities
|
|
240
232
|
* @returns {Object} Skill with frontmatter, title, stages array, reference, dirname
|
|
241
233
|
*/
|
|
242
|
-
export function generateSkillMd(skillData) {
|
|
234
|
+
export function generateSkillMd(skillData, stages) {
|
|
243
235
|
const { agent, name } = skillData;
|
|
244
236
|
|
|
245
237
|
if (!agent) {
|
|
@@ -250,17 +242,31 @@ export function generateSkillMd(skillData) {
|
|
|
250
242
|
throw new Error(`Skill ${skillData.id} agent section missing stages`);
|
|
251
243
|
}
|
|
252
244
|
|
|
245
|
+
// Build stage lookup map
|
|
246
|
+
const stageMap = new Map(stages.map((s) => [s.id, s]));
|
|
247
|
+
|
|
253
248
|
// Transform stages object to array for template rendering
|
|
254
249
|
const stagesArray = Object.entries(agent.stages).map(
|
|
255
250
|
([stageId, stageData]) => {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
const stageEntity = stageMap.get(stageId);
|
|
252
|
+
const stageName = stageEntity?.name || stageId;
|
|
253
|
+
|
|
254
|
+
// Find next stage from handoffs
|
|
255
|
+
let nextStageName = "Complete";
|
|
256
|
+
if (stageEntity?.handoffs) {
|
|
257
|
+
const nextHandoff = stageEntity.handoffs.find(
|
|
258
|
+
(h) => h.targetStage !== stageId,
|
|
259
|
+
);
|
|
260
|
+
if (nextHandoff) {
|
|
261
|
+
const nextStage = stageMap.get(nextHandoff.targetStage);
|
|
262
|
+
nextStageName = nextStage?.name || nextHandoff.targetStage;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
260
266
|
return {
|
|
261
267
|
stageId,
|
|
262
|
-
stageName
|
|
263
|
-
nextStageName
|
|
268
|
+
stageName,
|
|
269
|
+
nextStageName,
|
|
264
270
|
focus: stageData.focus,
|
|
265
271
|
activities: stageData.activities || [],
|
|
266
272
|
ready: stageData.ready || [],
|
|
@@ -277,11 +283,13 @@ export function generateSkillMd(skillData) {
|
|
|
277
283
|
return {
|
|
278
284
|
frontmatter: {
|
|
279
285
|
name: agent.name,
|
|
280
|
-
description: agent.description
|
|
286
|
+
description: agent.description,
|
|
287
|
+
useWhen: agent.useWhen || "",
|
|
281
288
|
},
|
|
282
289
|
title: name,
|
|
283
290
|
stages: stagesArray,
|
|
284
|
-
reference:
|
|
291
|
+
reference: skillData.implementationReference || "",
|
|
292
|
+
toolReferences: skillData.toolReferences || [],
|
|
285
293
|
dirname: agent.name,
|
|
286
294
|
};
|
|
287
295
|
}
|
package/app/model/derivation.js
CHANGED
|
@@ -376,17 +376,17 @@ export function isValidJobCombination({
|
|
|
376
376
|
* @returns {string} Generated job title
|
|
377
377
|
*/
|
|
378
378
|
export function generateJobTitle(discipline, grade, track = null) {
|
|
379
|
-
const { roleTitle,
|
|
379
|
+
const { roleTitle, isManagement } = discipline;
|
|
380
380
|
const { professionalTitle, managementTitle } = grade;
|
|
381
381
|
|
|
382
382
|
// Management discipline (no track needed)
|
|
383
383
|
if (isManagement && !track) {
|
|
384
|
-
return `${managementTitle}, ${
|
|
384
|
+
return `${managementTitle}, ${roleTitle}`;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
// Management discipline with track
|
|
388
388
|
if (isManagement && track) {
|
|
389
|
-
return `${managementTitle}, ${track.name}`;
|
|
389
|
+
return `${managementTitle}, ${roleTitle} – ${track.name}`;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
// IC discipline with track
|
package/app/model/levels.js
CHANGED
|
@@ -90,6 +90,7 @@ export const Capability = {
|
|
|
90
90
|
RELIABILITY: "reliability",
|
|
91
91
|
DATA: "data",
|
|
92
92
|
AI: "ai",
|
|
93
|
+
ML: "ml",
|
|
93
94
|
PROCESS: "process",
|
|
94
95
|
BUSINESS: "business",
|
|
95
96
|
PEOPLE: "people",
|
|
@@ -111,6 +112,7 @@ export const CAPABILITY_ORDER = [
|
|
|
111
112
|
Capability.DELIVERY,
|
|
112
113
|
Capability.DATA,
|
|
113
114
|
Capability.AI,
|
|
115
|
+
Capability.ML,
|
|
114
116
|
Capability.SCALE,
|
|
115
117
|
Capability.RELIABILITY,
|
|
116
118
|
Capability.PEOPLE,
|
package/app/model/loader.js
CHANGED
|
@@ -84,7 +84,15 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
84
84
|
|
|
85
85
|
if (capability.skills && Array.isArray(capability.skills)) {
|
|
86
86
|
for (const skill of capability.skills) {
|
|
87
|
-
const {
|
|
87
|
+
const {
|
|
88
|
+
id,
|
|
89
|
+
name,
|
|
90
|
+
isHumanOnly,
|
|
91
|
+
human,
|
|
92
|
+
agent,
|
|
93
|
+
implementationReference,
|
|
94
|
+
toolReferences,
|
|
95
|
+
} = skill;
|
|
88
96
|
allSkills.push({
|
|
89
97
|
id,
|
|
90
98
|
name,
|
|
@@ -95,6 +103,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
95
103
|
...(isHumanOnly && { isHumanOnly }),
|
|
96
104
|
// Preserve agent section for agent generation
|
|
97
105
|
...(agent && { agent }),
|
|
106
|
+
// Include implementation reference and tool references (shared by human and agent)
|
|
107
|
+
...(implementationReference && { implementationReference }),
|
|
108
|
+
...(toolReferences && { toolReferences }),
|
|
98
109
|
});
|
|
99
110
|
}
|
|
100
111
|
}
|
package/app/model/validation.js
CHANGED
|
@@ -241,17 +241,13 @@ function validateSkill(skill, index, requiredStageIds = []) {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
//
|
|
245
|
-
if (
|
|
246
|
-
skill.agent.reference !== undefined &&
|
|
247
|
-
typeof skill.agent.reference !== "string"
|
|
248
|
-
) {
|
|
244
|
+
// Error if old 'reference' field is still present (moved to skill.implementationReference)
|
|
245
|
+
if (skill.agent.reference !== undefined) {
|
|
249
246
|
errors.push(
|
|
250
247
|
createError(
|
|
251
|
-
"
|
|
252
|
-
"Skill agent reference
|
|
248
|
+
"INVALID_FIELD",
|
|
249
|
+
"Skill agent 'reference' field is not supported. Use skill.implementationReference instead.",
|
|
253
250
|
`${agentPath}.reference`,
|
|
254
|
-
skill.agent.reference,
|
|
255
251
|
),
|
|
256
252
|
);
|
|
257
253
|
}
|
|
@@ -295,6 +291,76 @@ function validateSkill(skill, index, requiredStageIds = []) {
|
|
|
295
291
|
}
|
|
296
292
|
}
|
|
297
293
|
|
|
294
|
+
// Validate implementationReference if present (optional string)
|
|
295
|
+
if (
|
|
296
|
+
skill.implementationReference !== undefined &&
|
|
297
|
+
typeof skill.implementationReference !== "string"
|
|
298
|
+
) {
|
|
299
|
+
errors.push(
|
|
300
|
+
createError(
|
|
301
|
+
"INVALID_VALUE",
|
|
302
|
+
"Skill implementationReference must be a string",
|
|
303
|
+
`${path}.implementationReference`,
|
|
304
|
+
skill.implementationReference,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Validate toolReferences array if present
|
|
310
|
+
if (skill.toolReferences !== undefined) {
|
|
311
|
+
if (!Array.isArray(skill.toolReferences)) {
|
|
312
|
+
errors.push(
|
|
313
|
+
createError(
|
|
314
|
+
"INVALID_VALUE",
|
|
315
|
+
"Skill toolReferences must be an array",
|
|
316
|
+
`${path}.toolReferences`,
|
|
317
|
+
skill.toolReferences,
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
skill.toolReferences.forEach((tool, i) => {
|
|
322
|
+
const toolPath = `${path}.toolReferences[${i}]`;
|
|
323
|
+
if (!tool.name) {
|
|
324
|
+
errors.push(
|
|
325
|
+
createError(
|
|
326
|
+
"MISSING_REQUIRED",
|
|
327
|
+
"Tool reference missing name",
|
|
328
|
+
`${toolPath}.name`,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (!tool.description) {
|
|
333
|
+
errors.push(
|
|
334
|
+
createError(
|
|
335
|
+
"MISSING_REQUIRED",
|
|
336
|
+
"Tool reference missing description",
|
|
337
|
+
`${toolPath}.description`,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (!tool.useWhen) {
|
|
342
|
+
errors.push(
|
|
343
|
+
createError(
|
|
344
|
+
"MISSING_REQUIRED",
|
|
345
|
+
"Tool reference missing useWhen",
|
|
346
|
+
`${toolPath}.useWhen`,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (tool.url !== undefined && typeof tool.url !== "string") {
|
|
351
|
+
errors.push(
|
|
352
|
+
createError(
|
|
353
|
+
"INVALID_VALUE",
|
|
354
|
+
"Tool reference url must be a string",
|
|
355
|
+
`${toolPath}.url`,
|
|
356
|
+
tool.url,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
298
364
|
return { errors, warnings };
|
|
299
365
|
}
|
|
300
366
|
|
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
deriveAgentSkills,
|
|
27
27
|
deriveReferenceGrade,
|
|
28
28
|
} from "../model/agent.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createSelectWithValue,
|
|
31
|
+
createDisciplineSelect,
|
|
32
|
+
} from "../lib/form-controls.js";
|
|
30
33
|
import { createReactive } from "../lib/reactive.js";
|
|
31
34
|
import { getStageEmoji } from "../formatters/stage/shared.js";
|
|
32
35
|
import { formatAgentProfile } from "../formatters/agent/profile.js";
|
|
@@ -242,9 +245,9 @@ export async function renderAgentBuilder() {
|
|
|
242
245
|
{ className: "form-group" },
|
|
243
246
|
label({ className: "form-label" }, "Discipline"),
|
|
244
247
|
availableDisciplines.length > 0
|
|
245
|
-
?
|
|
248
|
+
? createDisciplineSelect({
|
|
246
249
|
id: "agent-discipline-select",
|
|
247
|
-
|
|
250
|
+
disciplines: availableDisciplines,
|
|
248
251
|
initialValue: selection.get().discipline,
|
|
249
252
|
placeholder: "Select a discipline...",
|
|
250
253
|
onChange: (value) => {
|
|
@@ -404,7 +407,7 @@ function createAllStagesPreview(context) {
|
|
|
404
407
|
const skillFiles = derivedSkills
|
|
405
408
|
.map((derived) => skills.find((s) => s.id === derived.skillId))
|
|
406
409
|
.filter((skill) => skill?.agent)
|
|
407
|
-
.map((skill) => generateSkillMd(skill));
|
|
410
|
+
.map((skill) => generateSkillMd(skill, stages));
|
|
408
411
|
|
|
409
412
|
return div(
|
|
410
413
|
{ className: "agent-deployment" },
|
|
@@ -519,7 +522,7 @@ function createSingleStagePreview(context, stage) {
|
|
|
519
522
|
const skillFiles = derivedSkills
|
|
520
523
|
.map((d) => skills.find((s) => s.id === d.skillId))
|
|
521
524
|
.filter((skill) => skill?.agent)
|
|
522
|
-
.map((skill) => generateSkillMd(skill));
|
|
525
|
+
.map((skill) => generateSkillMd(skill, stages));
|
|
523
526
|
|
|
524
527
|
return div(
|
|
525
528
|
{ className: "agent-deployment" },
|