@forwardimpact/pathway 0.25.21 → 0.25.22
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/package.json +1 -1
- package/src/commands/agent.js +12 -10
- package/src/commands/build-bundle.js +88 -0
- package/src/commands/build-packs.js +392 -0
- package/src/commands/build.js +26 -83
- package/src/commands/job.js +5 -1
- package/src/commands/skill.js +1 -1
- package/src/css/pages/agent-builder.css +48 -0
- package/src/formatters/interview/shared.js +6 -4
- package/src/formatters/progress/shared.js +9 -20
- package/src/formatters/skill/shared.js +9 -2
- package/src/formatters/track/shared.js +4 -1
- package/src/pages/agent-builder-install.js +117 -0
- package/src/pages/agent-builder-preview.js +3 -3
- package/src/pages/agent-builder.js +23 -1
- package/src/pages/progress.js +3 -3
- package/src/pages/skill.js +5 -2
- package/src/lib/job-cache.js +0 -89
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -32,11 +32,11 @@ import {
|
|
|
32
32
|
deriveReferenceLevel,
|
|
33
33
|
deriveAgentSkills,
|
|
34
34
|
generateSkillMarkdown,
|
|
35
|
-
deriveToolkit,
|
|
36
35
|
getDisciplineAbbreviation,
|
|
37
36
|
toKebabCase,
|
|
38
37
|
interpolateTeamInstructions,
|
|
39
|
-
} from "@forwardimpact/libskill";
|
|
38
|
+
} from "@forwardimpact/libskill/agent";
|
|
39
|
+
import { deriveToolkit } from "@forwardimpact/libskill/toolkit";
|
|
40
40
|
import { formatAgentProfile } from "../formatters/agent/profile.js";
|
|
41
41
|
import { formatError, formatSuccess } from "../lib/cli-output.js";
|
|
42
42
|
import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
|
|
@@ -94,7 +94,7 @@ function showAgentSummary(data, agentData, skillsWithAgent) {
|
|
|
94
94
|
* @param {Object} agentData - Agent-specific data
|
|
95
95
|
* @returns {Array<{discipline: Object, track: Object, humanDiscipline: Object, humanTrack: Object}>}
|
|
96
96
|
*/
|
|
97
|
-
function findValidCombinations(data, agentData) {
|
|
97
|
+
export function findValidCombinations(data, agentData) {
|
|
98
98
|
const pairs = [];
|
|
99
99
|
for (const discipline of agentData.disciplines) {
|
|
100
100
|
for (const track of agentData.tracks) {
|
|
@@ -237,10 +237,10 @@ function resolveAgentEntities(data, agentData, disciplineId, trackId) {
|
|
|
237
237
|
* @param {Object} humanDiscipline
|
|
238
238
|
*/
|
|
239
239
|
function printTeamInstructions(agentTrack, humanDiscipline) {
|
|
240
|
-
const teamInstructions = interpolateTeamInstructions(
|
|
240
|
+
const teamInstructions = interpolateTeamInstructions({
|
|
241
241
|
agentTrack,
|
|
242
242
|
humanDiscipline,
|
|
243
|
-
);
|
|
243
|
+
});
|
|
244
244
|
if (teamInstructions) {
|
|
245
245
|
console.log("# Team Instructions (CLAUDE.md)\n");
|
|
246
246
|
console.log(teamInstructions.trim());
|
|
@@ -287,10 +287,10 @@ async function handleSingleStage({
|
|
|
287
287
|
return;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
const teamInstructions = interpolateTeamInstructions(
|
|
290
|
+
const teamInstructions = interpolateTeamInstructions({
|
|
291
291
|
agentTrack,
|
|
292
292
|
humanDiscipline,
|
|
293
|
-
);
|
|
293
|
+
});
|
|
294
294
|
await writeTeamInstructions(teamInstructions, baseDir);
|
|
295
295
|
await writeProfile(profile, baseDir, agentTemplate);
|
|
296
296
|
await generateClaudeCodeSettings(baseDir, agentData.claudeCodeSettings);
|
|
@@ -331,7 +331,9 @@ async function handleAllStages({
|
|
|
331
331
|
const skillFiles = derivedSkills
|
|
332
332
|
.map((derived) => skillsWithAgent.find((s) => s.id === derived.skillId))
|
|
333
333
|
.filter((skill) => skill?.agent)
|
|
334
|
-
.map((skill) =>
|
|
334
|
+
.map((skill) =>
|
|
335
|
+
generateSkillMarkdown({ skillData: skill, stages: data.stages }),
|
|
336
|
+
);
|
|
335
337
|
|
|
336
338
|
for (const profile of profiles) {
|
|
337
339
|
const errors = validateAgentProfile(profile);
|
|
@@ -373,10 +375,10 @@ async function handleAllStages({
|
|
|
373
375
|
return;
|
|
374
376
|
}
|
|
375
377
|
|
|
376
|
-
const teamInstructions = interpolateTeamInstructions(
|
|
378
|
+
const teamInstructions = interpolateTeamInstructions({
|
|
377
379
|
agentTrack,
|
|
378
380
|
humanDiscipline,
|
|
379
|
-
);
|
|
381
|
+
});
|
|
380
382
|
await writeTeamInstructions(teamInstructions, baseDir);
|
|
381
383
|
for (const profile of profiles) {
|
|
382
384
|
await writeProfile(profile, baseDir, agentTemplate);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle generation for Pathway distribution.
|
|
3
|
+
*
|
|
4
|
+
* Emits `bundle.tar.gz` and `install.sh` alongside the static site so
|
|
5
|
+
* engineers can install `@forwardimpact/pathway` globally via
|
|
6
|
+
* `curl -fsSL <site>/install.sh | bash`. Invoked from build.js when
|
|
7
|
+
* `framework.distribution.siteUrl` is configured.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { cp, mkdir, rm, readFile, writeFile } from "fs/promises";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { execFileSync } from "child_process";
|
|
13
|
+
import Mustache from "mustache";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate distribution bundle (bundle.tar.gz + install.sh)
|
|
17
|
+
* @param {Object} params
|
|
18
|
+
* @param {string} params.outputDir - Build output directory
|
|
19
|
+
* @param {string} params.dataDir - Source data directory
|
|
20
|
+
* @param {string} params.siteUrl - Base URL for the published site
|
|
21
|
+
* @param {Object} params.framework - Framework configuration
|
|
22
|
+
* @param {string} params.version - Pathway package version
|
|
23
|
+
* @param {string} params.templatesDir - Absolute path to pathway/templates
|
|
24
|
+
*/
|
|
25
|
+
export async function generateBundle({
|
|
26
|
+
outputDir,
|
|
27
|
+
dataDir,
|
|
28
|
+
siteUrl,
|
|
29
|
+
framework,
|
|
30
|
+
version,
|
|
31
|
+
templatesDir,
|
|
32
|
+
}) {
|
|
33
|
+
console.log("📦 Generating distribution bundle...");
|
|
34
|
+
|
|
35
|
+
const frameworkTitle = framework.title || "Engineering Pathway";
|
|
36
|
+
|
|
37
|
+
// 1. Create temporary bundle directory
|
|
38
|
+
const bundleDir = join(outputDir, "_bundle");
|
|
39
|
+
await mkdir(bundleDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
// 2. Generate minimal package.json for the bundle
|
|
42
|
+
const bundlePkg = {
|
|
43
|
+
name: "fit-pathway-local",
|
|
44
|
+
version: version,
|
|
45
|
+
private: true,
|
|
46
|
+
dependencies: {
|
|
47
|
+
"@forwardimpact/pathway": `^${version}`,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
await writeFile(
|
|
51
|
+
join(bundleDir, "package.json"),
|
|
52
|
+
JSON.stringify(bundlePkg, null, 2) + "\n",
|
|
53
|
+
);
|
|
54
|
+
console.log(` ✓ package.json (pathway ^${version})`);
|
|
55
|
+
|
|
56
|
+
// 3. Copy data files into bundle
|
|
57
|
+
await cp(dataDir, join(bundleDir, "data"), {
|
|
58
|
+
recursive: true,
|
|
59
|
+
dereference: true,
|
|
60
|
+
});
|
|
61
|
+
console.log(" ✓ data/");
|
|
62
|
+
|
|
63
|
+
// 4. Create tar.gz from the bundle directory
|
|
64
|
+
execFileSync("tar", [
|
|
65
|
+
"-czf",
|
|
66
|
+
join(outputDir, "bundle.tar.gz"),
|
|
67
|
+
"-C",
|
|
68
|
+
outputDir,
|
|
69
|
+
"_bundle",
|
|
70
|
+
]);
|
|
71
|
+
console.log(" ✓ bundle.tar.gz");
|
|
72
|
+
|
|
73
|
+
// 5. Clean up temporary bundle directory
|
|
74
|
+
await rm(bundleDir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
// 6. Render install.sh from template
|
|
77
|
+
const templatePath = join(templatesDir, "install.template.sh");
|
|
78
|
+
const template = await readFile(templatePath, "utf8");
|
|
79
|
+
const installScript = Mustache.render(template, {
|
|
80
|
+
siteUrl: siteUrl.replace(/\/$/, ""),
|
|
81
|
+
version,
|
|
82
|
+
frameworkTitle,
|
|
83
|
+
});
|
|
84
|
+
await writeFile(join(outputDir, "install.sh"), installScript, {
|
|
85
|
+
mode: 0o755,
|
|
86
|
+
});
|
|
87
|
+
console.log(" ✓ install.sh");
|
|
88
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack generation for Pathway distribution.
|
|
3
|
+
*
|
|
4
|
+
* Emits one pre-built agent/skill pack per valid discipline/track combination
|
|
5
|
+
* and two discovery manifests (`.well-known/agent-skills/index.json` for
|
|
6
|
+
* `npx skills` and `apm.yml` for Microsoft APM). See
|
|
7
|
+
* specs/320-pathway-ecosystem-distribution for context.
|
|
8
|
+
*
|
|
9
|
+
* Invoked from build.js after the distribution bundle has been generated.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdir, rm, readFile, writeFile } from "fs/promises";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { execFileSync } from "child_process";
|
|
15
|
+
import { createHash } from "crypto";
|
|
16
|
+
|
|
17
|
+
import { createDataLoader } from "@forwardimpact/map/loader";
|
|
18
|
+
import { createTemplateLoader } from "@forwardimpact/libtemplate";
|
|
19
|
+
import {
|
|
20
|
+
generateStageAgentProfile,
|
|
21
|
+
deriveReferenceLevel,
|
|
22
|
+
deriveAgentSkills,
|
|
23
|
+
generateSkillMarkdown,
|
|
24
|
+
interpolateTeamInstructions,
|
|
25
|
+
getDisciplineAbbreviation,
|
|
26
|
+
toKebabCase,
|
|
27
|
+
} from "@forwardimpact/libskill/agent";
|
|
28
|
+
|
|
29
|
+
import { formatAgentProfile } from "../formatters/agent/profile.js";
|
|
30
|
+
import {
|
|
31
|
+
formatAgentSkill,
|
|
32
|
+
formatInstallScript,
|
|
33
|
+
formatReference,
|
|
34
|
+
} from "../formatters/agent/skill.js";
|
|
35
|
+
import { findValidCombinations } from "./agent.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Slugify a string for use as a package name.
|
|
39
|
+
* @param {string} text
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function slugify(text) {
|
|
43
|
+
return text
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
46
|
+
.replace(/^-+|-+$/g, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stringify a JSON value with object keys sorted recursively.
|
|
51
|
+
* Produces deterministic output for digest stability.
|
|
52
|
+
* @param {unknown} value
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function stringifySorted(value) {
|
|
56
|
+
const seen = new WeakSet();
|
|
57
|
+
const sort = (v) => {
|
|
58
|
+
if (v === null || typeof v !== "object") return v;
|
|
59
|
+
if (seen.has(v)) throw new Error("Cannot stringify circular structure");
|
|
60
|
+
seen.add(v);
|
|
61
|
+
if (Array.isArray(v)) return v.map(sort);
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const key of Object.keys(v).sort()) out[key] = sort(v[key]);
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
return JSON.stringify(sort(value), null, 2) + "\n";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Escape a YAML scalar double-quoted value.
|
|
71
|
+
* @param {string} text
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function yamlQuote(text) {
|
|
75
|
+
return `"${String(text).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Write a single pack's files to disk under the staging directory.
|
|
80
|
+
* Calls the formatters directly (silent, no console output).
|
|
81
|
+
* @param {Object} params
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async function writePackFiles({
|
|
85
|
+
packDir,
|
|
86
|
+
profiles,
|
|
87
|
+
skillFiles,
|
|
88
|
+
teamInstructions,
|
|
89
|
+
agentTemplate,
|
|
90
|
+
skillTemplates,
|
|
91
|
+
claudeCodeSettings,
|
|
92
|
+
}) {
|
|
93
|
+
const claudeDir = join(packDir, ".claude");
|
|
94
|
+
const agentsDir = join(claudeDir, "agents");
|
|
95
|
+
const skillsDir = join(claudeDir, "skills");
|
|
96
|
+
|
|
97
|
+
await mkdir(agentsDir, { recursive: true });
|
|
98
|
+
await mkdir(skillsDir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
for (const profile of profiles) {
|
|
101
|
+
const profilePath = join(agentsDir, profile.filename);
|
|
102
|
+
await writeFile(
|
|
103
|
+
profilePath,
|
|
104
|
+
formatAgentProfile(profile, agentTemplate),
|
|
105
|
+
"utf-8",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const skill of skillFiles) {
|
|
110
|
+
const skillDir = join(skillsDir, skill.dirname);
|
|
111
|
+
await mkdir(skillDir, { recursive: true });
|
|
112
|
+
|
|
113
|
+
await writeFile(
|
|
114
|
+
join(skillDir, "SKILL.md"),
|
|
115
|
+
formatAgentSkill(skill, skillTemplates.skill),
|
|
116
|
+
"utf-8",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (skill.installScript) {
|
|
120
|
+
const scriptsDir = join(skillDir, "scripts");
|
|
121
|
+
await mkdir(scriptsDir, { recursive: true });
|
|
122
|
+
await writeFile(
|
|
123
|
+
join(scriptsDir, "install.sh"),
|
|
124
|
+
formatInstallScript(skill, skillTemplates.install),
|
|
125
|
+
{ mode: 0o755 },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (skill.implementationReference) {
|
|
130
|
+
const refsDir = join(skillDir, "references");
|
|
131
|
+
await mkdir(refsDir, { recursive: true });
|
|
132
|
+
await writeFile(
|
|
133
|
+
join(refsDir, "REFERENCE.md"),
|
|
134
|
+
formatReference(skill, skillTemplates.reference),
|
|
135
|
+
"utf-8",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (teamInstructions) {
|
|
141
|
+
await writeFile(
|
|
142
|
+
join(claudeDir, "CLAUDE.md"),
|
|
143
|
+
teamInstructions.trim() + "\n",
|
|
144
|
+
"utf-8",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Claude Code settings — matches the CLI path's generateClaudeCodeSettings
|
|
149
|
+
// output format (no merge with existing files since the staging dir starts
|
|
150
|
+
// empty).
|
|
151
|
+
const settings = { ...(claudeCodeSettings || {}) };
|
|
152
|
+
await writeFile(
|
|
153
|
+
join(claudeDir, "settings.json"),
|
|
154
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
155
|
+
"utf-8",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Derive profiles, skills, and team instructions for a single combination.
|
|
161
|
+
* @param {Object} params
|
|
162
|
+
* @returns {{profiles: Array, skillFiles: Array, teamInstructions: string|null}}
|
|
163
|
+
*/
|
|
164
|
+
function derivePackContent({
|
|
165
|
+
discipline,
|
|
166
|
+
track,
|
|
167
|
+
humanDiscipline,
|
|
168
|
+
humanTrack,
|
|
169
|
+
data,
|
|
170
|
+
agentData,
|
|
171
|
+
skillsWithAgent,
|
|
172
|
+
level,
|
|
173
|
+
}) {
|
|
174
|
+
const stageParams = {
|
|
175
|
+
discipline: humanDiscipline,
|
|
176
|
+
track: humanTrack,
|
|
177
|
+
level,
|
|
178
|
+
skills: skillsWithAgent,
|
|
179
|
+
behaviours: data.behaviours,
|
|
180
|
+
agentBehaviours: agentData.behaviours,
|
|
181
|
+
agentDiscipline: discipline,
|
|
182
|
+
agentTrack: track,
|
|
183
|
+
stages: data.stages,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const profiles = data.stages.map((stage) =>
|
|
187
|
+
generateStageAgentProfile({ ...stageParams, stage }),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const derivedSkills = deriveAgentSkills({
|
|
191
|
+
discipline: humanDiscipline,
|
|
192
|
+
track: humanTrack,
|
|
193
|
+
level,
|
|
194
|
+
skills: skillsWithAgent,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const skillFiles = derivedSkills
|
|
198
|
+
.map((derived) => skillsWithAgent.find((s) => s.id === derived.skillId))
|
|
199
|
+
.filter((skill) => skill?.agent)
|
|
200
|
+
.map((skill) =>
|
|
201
|
+
generateSkillMarkdown({ skillData: skill, stages: data.stages }),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const teamInstructions = interpolateTeamInstructions({
|
|
205
|
+
agentTrack: track,
|
|
206
|
+
humanDiscipline,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return { profiles, skillFiles, teamInstructions };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Archive a staged pack directory as a deterministic tar.gz and return its
|
|
214
|
+
* sha256 digest.
|
|
215
|
+
* @param {string} packDir - Staging directory containing the pack files
|
|
216
|
+
* @param {string} archivePath - Destination path for the tar.gz
|
|
217
|
+
* @returns {Promise<string>} sha256 digest string (e.g. "sha256:abc...")
|
|
218
|
+
*/
|
|
219
|
+
async function archivePack(packDir, archivePath) {
|
|
220
|
+
execFileSync("tar", [
|
|
221
|
+
"-czf",
|
|
222
|
+
archivePath,
|
|
223
|
+
"--sort=name",
|
|
224
|
+
"--mtime=1970-01-01",
|
|
225
|
+
"--owner=0",
|
|
226
|
+
"--group=0",
|
|
227
|
+
"--numeric-owner",
|
|
228
|
+
"-C",
|
|
229
|
+
packDir,
|
|
230
|
+
".",
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const bytes = await readFile(archivePath);
|
|
234
|
+
return "sha256:" + createHash("sha256").update(bytes).digest("hex");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Write the `npx skills` discovery manifest.
|
|
239
|
+
* @param {string} outputDir
|
|
240
|
+
* @param {Array<{name: string, description: string, url: string, digest: string}>} packs
|
|
241
|
+
* @param {string} version
|
|
242
|
+
*/
|
|
243
|
+
async function writeSkillsManifest(outputDir, packs, version) {
|
|
244
|
+
const wellKnownDir = join(outputDir, ".well-known", "agent-skills");
|
|
245
|
+
await mkdir(wellKnownDir, { recursive: true });
|
|
246
|
+
const manifest = {
|
|
247
|
+
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
248
|
+
skills: packs.map((pack) => ({
|
|
249
|
+
description: pack.description,
|
|
250
|
+
digest: pack.digest,
|
|
251
|
+
name: pack.name,
|
|
252
|
+
type: "archive",
|
|
253
|
+
url: pack.url,
|
|
254
|
+
version,
|
|
255
|
+
})),
|
|
256
|
+
};
|
|
257
|
+
await writeFile(
|
|
258
|
+
join(wellKnownDir, "index.json"),
|
|
259
|
+
stringifySorted(manifest),
|
|
260
|
+
"utf-8",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Write the Microsoft APM manifest at the site root.
|
|
266
|
+
* @param {string} outputDir
|
|
267
|
+
* @param {Array<{name: string, description: string, url: string, digest: string}>} packs
|
|
268
|
+
* @param {string} version
|
|
269
|
+
* @param {string} frameworkTitle
|
|
270
|
+
*/
|
|
271
|
+
async function writeApmManifest(outputDir, packs, version, frameworkTitle) {
|
|
272
|
+
const lines = [
|
|
273
|
+
`name: ${slugify(frameworkTitle)}`,
|
|
274
|
+
`version: ${version}`,
|
|
275
|
+
`description: ${yamlQuote(`${frameworkTitle} agent teams for Claude Code`)}`,
|
|
276
|
+
"",
|
|
277
|
+
"skills:",
|
|
278
|
+
];
|
|
279
|
+
for (const pack of packs) {
|
|
280
|
+
lines.push(` - name: ${pack.name}`);
|
|
281
|
+
lines.push(` description: ${yamlQuote(pack.description)}`);
|
|
282
|
+
lines.push(` version: ${version}`);
|
|
283
|
+
lines.push(` url: ${yamlQuote(pack.url)}`);
|
|
284
|
+
lines.push(` digest: ${yamlQuote(pack.digest)}`);
|
|
285
|
+
}
|
|
286
|
+
lines.push("");
|
|
287
|
+
await writeFile(join(outputDir, "apm.yml"), lines.join("\n"), "utf-8");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate pre-built agent/skill packs for installation through ecosystem
|
|
292
|
+
* tools like `npx skills` and Microsoft APM. One pack per valid
|
|
293
|
+
* discipline/track combination.
|
|
294
|
+
*
|
|
295
|
+
* @param {Object} params
|
|
296
|
+
* @param {string} params.outputDir - Build output directory
|
|
297
|
+
* @param {string} params.dataDir - Source data directory
|
|
298
|
+
* @param {string} params.siteUrl - Base URL for the published site
|
|
299
|
+
* @param {Object} params.framework - Framework configuration
|
|
300
|
+
* @param {string} params.version - Pathway package version
|
|
301
|
+
* @param {string} params.templatesDir - Absolute path to pathway/templates
|
|
302
|
+
*/
|
|
303
|
+
export async function generatePacks({
|
|
304
|
+
outputDir,
|
|
305
|
+
dataDir,
|
|
306
|
+
siteUrl,
|
|
307
|
+
framework,
|
|
308
|
+
version,
|
|
309
|
+
templatesDir,
|
|
310
|
+
}) {
|
|
311
|
+
console.log("📦 Generating agent/skill packs...");
|
|
312
|
+
|
|
313
|
+
const normalizedSiteUrl = siteUrl.replace(/\/$/, "");
|
|
314
|
+
const frameworkTitle = framework.title || "Engineering Pathway";
|
|
315
|
+
|
|
316
|
+
const loader = createDataLoader();
|
|
317
|
+
const templateLoader = createTemplateLoader(templatesDir);
|
|
318
|
+
|
|
319
|
+
const data = await loader.loadAllData(dataDir);
|
|
320
|
+
const agentData = await loader.loadAgentData(dataDir);
|
|
321
|
+
const skillsWithAgent = await loader.loadSkillsWithAgentData(dataDir);
|
|
322
|
+
|
|
323
|
+
const level = deriveReferenceLevel(data.levels);
|
|
324
|
+
|
|
325
|
+
const agentTemplate = templateLoader.load("agent.template.md", dataDir);
|
|
326
|
+
const skillTemplates = {
|
|
327
|
+
skill: templateLoader.load("skill.template.md", dataDir),
|
|
328
|
+
install: templateLoader.load("skill-install.template.sh", dataDir),
|
|
329
|
+
reference: templateLoader.load("skill-reference.template.md", dataDir),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const stagingDir = join(outputDir, "_packs");
|
|
333
|
+
const packsDir = join(outputDir, "packs");
|
|
334
|
+
await mkdir(stagingDir, { recursive: true });
|
|
335
|
+
await mkdir(packsDir, { recursive: true });
|
|
336
|
+
|
|
337
|
+
const combinations = findValidCombinations(data, agentData);
|
|
338
|
+
if (combinations.length === 0) {
|
|
339
|
+
console.log(" (no valid discipline/track combinations — skipping)");
|
|
340
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const packs = [];
|
|
345
|
+
|
|
346
|
+
for (const combination of combinations) {
|
|
347
|
+
const { discipline, track, humanDiscipline, humanTrack } = combination;
|
|
348
|
+
const abbrev = getDisciplineAbbreviation(discipline.id);
|
|
349
|
+
const agentName = `${abbrev}-${toKebabCase(track.id)}`;
|
|
350
|
+
const specName = humanDiscipline.specialization || humanDiscipline.name;
|
|
351
|
+
const description = `${specName} (${humanTrack.name}) — agent team`;
|
|
352
|
+
|
|
353
|
+
const { profiles, skillFiles, teamInstructions } = derivePackContent({
|
|
354
|
+
...combination,
|
|
355
|
+
data,
|
|
356
|
+
agentData,
|
|
357
|
+
skillsWithAgent,
|
|
358
|
+
level,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const packDir = join(stagingDir, agentName);
|
|
362
|
+
await writePackFiles({
|
|
363
|
+
packDir,
|
|
364
|
+
profiles,
|
|
365
|
+
skillFiles,
|
|
366
|
+
teamInstructions,
|
|
367
|
+
agentTemplate,
|
|
368
|
+
skillTemplates,
|
|
369
|
+
claudeCodeSettings: agentData.claudeCodeSettings,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const archivePath = join(packsDir, `${agentName}.tar.gz`);
|
|
373
|
+
const digest = await archivePack(packDir, archivePath);
|
|
374
|
+
|
|
375
|
+
packs.push({
|
|
376
|
+
name: agentName,
|
|
377
|
+
description,
|
|
378
|
+
url: `${normalizedSiteUrl}/packs/${agentName}.tar.gz`,
|
|
379
|
+
digest,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
console.log(` ✓ packs/${agentName}.tar.gz`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
386
|
+
|
|
387
|
+
await writeSkillsManifest(outputDir, packs, version);
|
|
388
|
+
console.log(" ✓ .well-known/agent-skills/index.json");
|
|
389
|
+
|
|
390
|
+
await writeApmManifest(outputDir, packs, version, frameworkTitle);
|
|
391
|
+
console.log(" ✓ apm.yml");
|
|
392
|
+
}
|
package/src/commands/build.js
CHANGED
|
@@ -3,26 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates a static site from the Engineering Pathway data.
|
|
5
5
|
* Copies all necessary files (HTML, JS, CSS) and data to an output directory.
|
|
6
|
-
* Optionally
|
|
7
|
-
*
|
|
6
|
+
* Optionally delegates to build-bundle and build-packs to produce the
|
|
7
|
+
* distribution surfaces (bundle.tar.gz + install.sh for the curl|bash flow,
|
|
8
|
+
* and agent/skill packs for ecosystem tools like `npx skills` and APM) when
|
|
9
|
+
* `framework.distribution.siteUrl` is configured.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
import {
|
|
11
|
-
cp,
|
|
12
|
-
mkdir,
|
|
13
|
-
rm,
|
|
14
|
-
access,
|
|
15
|
-
realpath,
|
|
16
|
-
readFile,
|
|
17
|
-
writeFile,
|
|
18
|
-
} from "fs/promises";
|
|
12
|
+
import { cp, mkdir, rm, access, realpath, writeFile } from "fs/promises";
|
|
19
13
|
import { readFileSync } from "fs";
|
|
20
14
|
import { join, dirname, relative, resolve } from "path";
|
|
21
15
|
import { fileURLToPath } from "url";
|
|
22
|
-
import { execFileSync } from "child_process";
|
|
23
|
-
import Mustache from "mustache";
|
|
24
16
|
import { createIndexGenerator } from "@forwardimpact/map/index-generator";
|
|
25
17
|
import { createDataLoader } from "@forwardimpact/map/loader";
|
|
18
|
+
import { generateBundle } from "./build-bundle.js";
|
|
19
|
+
import { generatePacks } from "./build-packs.js";
|
|
26
20
|
|
|
27
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
22
|
const __dirname = dirname(__filename);
|
|
@@ -201,10 +195,26 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
|
|
|
201
195
|
);
|
|
202
196
|
console.log(` ✓ version.json (${version})`);
|
|
203
197
|
|
|
204
|
-
// Generate distribution
|
|
198
|
+
// Generate distribution surfaces if siteUrl is configured
|
|
205
199
|
const siteUrl = options.url || framework.distribution?.siteUrl;
|
|
206
200
|
if (siteUrl) {
|
|
207
|
-
|
|
201
|
+
const templatesDir = join(appDir, "..", "templates");
|
|
202
|
+
await generateBundle({
|
|
203
|
+
outputDir,
|
|
204
|
+
dataDir,
|
|
205
|
+
siteUrl,
|
|
206
|
+
framework,
|
|
207
|
+
version,
|
|
208
|
+
templatesDir,
|
|
209
|
+
});
|
|
210
|
+
await generatePacks({
|
|
211
|
+
outputDir,
|
|
212
|
+
dataDir,
|
|
213
|
+
siteUrl,
|
|
214
|
+
framework,
|
|
215
|
+
version,
|
|
216
|
+
templatesDir,
|
|
217
|
+
});
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
// Show summary
|
|
@@ -212,7 +222,7 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
|
|
|
212
222
|
✅ Site generated successfully!
|
|
213
223
|
|
|
214
224
|
Output: ${outputDir}
|
|
215
|
-
${siteUrl ? `\nDistribution:\n ${outputDir}/bundle.tar.gz\n ${outputDir}/install.sh\n` : ""}
|
|
225
|
+
${siteUrl ? `\nDistribution:\n ${outputDir}/bundle.tar.gz\n ${outputDir}/install.sh\n ${outputDir}/packs/ (agent/skill packs)\n ${outputDir}/.well-known/agent-skills/index.json\n ${outputDir}/apm.yml\n` : ""}
|
|
216
226
|
To serve locally:
|
|
217
227
|
cd ${relative(process.cwd(), outputDir) || "."}
|
|
218
228
|
bunx serve .
|
|
@@ -228,70 +238,3 @@ function getPathwayVersion() {
|
|
|
228
238
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
229
239
|
return pkg.version;
|
|
230
240
|
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Generate distribution bundle (bundle.tar.gz + install.sh)
|
|
234
|
-
* @param {Object} params
|
|
235
|
-
* @param {string} params.outputDir - Build output directory
|
|
236
|
-
* @param {string} params.dataDir - Source data directory
|
|
237
|
-
* @param {string} params.siteUrl - Base URL for the published site
|
|
238
|
-
* @param {Object} params.framework - Framework configuration
|
|
239
|
-
*/
|
|
240
|
-
async function generateBundle({ outputDir, dataDir, siteUrl, framework }) {
|
|
241
|
-
console.log("📦 Generating distribution bundle...");
|
|
242
|
-
|
|
243
|
-
const version = getPathwayVersion();
|
|
244
|
-
const frameworkTitle = framework.title || "Engineering Pathway";
|
|
245
|
-
|
|
246
|
-
// 1. Create temporary bundle directory
|
|
247
|
-
const bundleDir = join(outputDir, "_bundle");
|
|
248
|
-
await mkdir(bundleDir, { recursive: true });
|
|
249
|
-
|
|
250
|
-
// 2. Generate minimal package.json for the bundle
|
|
251
|
-
const bundlePkg = {
|
|
252
|
-
name: "fit-pathway-local",
|
|
253
|
-
version: version,
|
|
254
|
-
private: true,
|
|
255
|
-
dependencies: {
|
|
256
|
-
"@forwardimpact/pathway": `^${version}`,
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
await writeFile(
|
|
260
|
-
join(bundleDir, "package.json"),
|
|
261
|
-
JSON.stringify(bundlePkg, null, 2) + "\n",
|
|
262
|
-
);
|
|
263
|
-
console.log(` ✓ package.json (pathway ^${version})`);
|
|
264
|
-
|
|
265
|
-
// 3. Copy data files into bundle
|
|
266
|
-
await cp(dataDir, join(bundleDir, "data"), {
|
|
267
|
-
recursive: true,
|
|
268
|
-
dereference: true,
|
|
269
|
-
});
|
|
270
|
-
console.log(" ✓ data/");
|
|
271
|
-
|
|
272
|
-
// 4. Create tar.gz from the bundle directory
|
|
273
|
-
execFileSync("tar", [
|
|
274
|
-
"-czf",
|
|
275
|
-
join(outputDir, "bundle.tar.gz"),
|
|
276
|
-
"-C",
|
|
277
|
-
outputDir,
|
|
278
|
-
"_bundle",
|
|
279
|
-
]);
|
|
280
|
-
console.log(" ✓ bundle.tar.gz");
|
|
281
|
-
|
|
282
|
-
// 5. Clean up temporary bundle directory
|
|
283
|
-
await rm(bundleDir, { recursive: true });
|
|
284
|
-
|
|
285
|
-
// 6. Render install.sh from template
|
|
286
|
-
const templatePath = join(appDir, "..", "templates", "install.template.sh");
|
|
287
|
-
const template = await readFile(templatePath, "utf8");
|
|
288
|
-
const installScript = Mustache.render(template, {
|
|
289
|
-
siteUrl: siteUrl.replace(/\/$/, ""),
|
|
290
|
-
version,
|
|
291
|
-
frameworkTitle,
|
|
292
|
-
});
|
|
293
|
-
await writeFile(join(outputDir, "install.sh"), installScript, {
|
|
294
|
-
mode: 0o755,
|
|
295
|
-
});
|
|
296
|
-
console.log(" ✓ install.sh");
|
|
297
|
-
}
|
package/src/commands/job.js
CHANGED
|
@@ -44,7 +44,11 @@ function formatJob(view, _options, entities, jobTemplate) {
|
|
|
44
44
|
*/
|
|
45
45
|
function printJobList(filteredJobs) {
|
|
46
46
|
for (const job of filteredJobs) {
|
|
47
|
-
const title = generateJobTitle(
|
|
47
|
+
const title = generateJobTitle({
|
|
48
|
+
discipline: job.discipline,
|
|
49
|
+
level: job.level,
|
|
50
|
+
track: job.track,
|
|
51
|
+
});
|
|
48
52
|
if (job.track) {
|
|
49
53
|
console.log(
|
|
50
54
|
`${job.discipline.id} ${job.level.id} ${job.track.id}, ${title}`,
|
package/src/commands/skill.js
CHANGED
|
@@ -79,7 +79,7 @@ async function formatAgentDetail(skill, stages, templateLoader, dataDir) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const template = templateLoader.load("skill.template.md", dataDir);
|
|
82
|
-
const skillMd = generateSkillMarkdown(skill, stages);
|
|
82
|
+
const skillMd = generateSkillMarkdown({ skillData: skill, stages });
|
|
83
83
|
const output = formatAgentSkill(skillMd, template);
|
|
84
84
|
console.log(output);
|
|
85
85
|
}
|
|
@@ -33,6 +33,54 @@
|
|
|
33
33
|
gap: var(--space-xl);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/* Install section — ecosystem-tool install commands shown between the
|
|
37
|
+
dropdowns and the preview cards. Uses the same surface treatment as
|
|
38
|
+
the form so the two blocks read as a matched pair. */
|
|
39
|
+
.agent-install-section {
|
|
40
|
+
background: var(--color-surface);
|
|
41
|
+
border-radius: var(--radius-lg);
|
|
42
|
+
padding: var(--space-xl);
|
|
43
|
+
box-shadow: var(--shadow-md);
|
|
44
|
+
margin-bottom: var(--space-xl);
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: var(--space-lg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.agent-install-header h2 {
|
|
51
|
+
margin: 0 0 var(--space-sm) 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.agent-install-description {
|
|
55
|
+
margin: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.agent-install-commands {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: var(--space-md);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.agent-install-command {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: var(--space-xs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.agent-install-command-label {
|
|
71
|
+
margin: 0;
|
|
72
|
+
font-size: var(--font-size-sm);
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
color: var(--color-text-secondary);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Allow the command prompts inside the install section to grow beyond the
|
|
78
|
+
600px center-aligned default used on the landing hero. */
|
|
79
|
+
.agent-install-command .command-prompt {
|
|
80
|
+
max-width: none;
|
|
81
|
+
margin: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
36
84
|
/* Agent section — spacing container, no surface */
|
|
37
85
|
.agent-section {
|
|
38
86
|
margin-bottom: var(--space-lg);
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
deriveDecompositionInterview,
|
|
15
15
|
deriveStakeholderInterview,
|
|
16
16
|
} from "@forwardimpact/libskill/interview";
|
|
17
|
-
import {
|
|
17
|
+
import { createJobCache } from "@forwardimpact/libskill/job-cache";
|
|
18
|
+
|
|
19
|
+
const jobCache = createJobCache();
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Interview type configurations
|
|
@@ -140,7 +142,7 @@ export function prepareInterviewDetail({
|
|
|
140
142
|
}) {
|
|
141
143
|
if (!discipline || !level) return null;
|
|
142
144
|
|
|
143
|
-
const job =
|
|
145
|
+
const job = jobCache.getOrCreate({
|
|
144
146
|
discipline,
|
|
145
147
|
level,
|
|
146
148
|
track,
|
|
@@ -275,7 +277,7 @@ export function prepareInterviewBuilderPreview({
|
|
|
275
277
|
};
|
|
276
278
|
}
|
|
277
279
|
|
|
278
|
-
const title = generateJobTitle(discipline, level, track);
|
|
280
|
+
const title = generateJobTitle({ discipline, level, track });
|
|
279
281
|
const totalSkills = getDisciplineSkillIds(discipline).length;
|
|
280
282
|
|
|
281
283
|
return {
|
|
@@ -320,7 +322,7 @@ export function prepareAllInterviews({
|
|
|
320
322
|
// Track is optional (null = generalist)
|
|
321
323
|
if (!discipline || !level) return null;
|
|
322
324
|
|
|
323
|
-
const job =
|
|
325
|
+
const job = jobCache.getOrCreate({
|
|
324
326
|
discipline,
|
|
325
327
|
level,
|
|
326
328
|
track,
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
analyzeCustomProgression,
|
|
14
14
|
getNextLevel,
|
|
15
15
|
} from "@forwardimpact/libskill/progression";
|
|
16
|
-
import {
|
|
16
|
+
import { createJobCache } from "@forwardimpact/libskill/job-cache";
|
|
17
|
+
|
|
18
|
+
const jobCache = createJobCache();
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Get the next level for progression
|
|
@@ -22,20 +24,7 @@ import { getOrCreateJob } from "@forwardimpact/libskill/job-cache";
|
|
|
22
24
|
* @returns {Object|null}
|
|
23
25
|
*/
|
|
24
26
|
export function getDefaultTargetLevel(currentLevel, levels) {
|
|
25
|
-
return getNextLevel(currentLevel, levels);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if a job combination is valid
|
|
30
|
-
* @param {Object} params
|
|
31
|
-
* @param {Object} params.discipline
|
|
32
|
-
* @param {Object} params.level
|
|
33
|
-
* @param {Object} params.track
|
|
34
|
-
* @param {Array} [params.levels] - All levels for validation
|
|
35
|
-
* @returns {boolean}
|
|
36
|
-
*/
|
|
37
|
-
export function isValidCombination({ discipline, level, track, levels }) {
|
|
38
|
-
return isValidJobCombination({ discipline, level, track, levels });
|
|
27
|
+
return getNextLevel({ level: currentLevel, levels });
|
|
39
28
|
}
|
|
40
29
|
|
|
41
30
|
/**
|
|
@@ -69,7 +58,7 @@ export function prepareCurrentJob({
|
|
|
69
58
|
}) {
|
|
70
59
|
if (!discipline || !level) return null;
|
|
71
60
|
|
|
72
|
-
const job =
|
|
61
|
+
const job = jobCache.getOrCreate({
|
|
73
62
|
discipline,
|
|
74
63
|
level,
|
|
75
64
|
track,
|
|
@@ -148,8 +137,8 @@ export function prepareCareerProgressPreview({
|
|
|
148
137
|
};
|
|
149
138
|
}
|
|
150
139
|
|
|
151
|
-
const title = generateJobTitle(discipline, level, track);
|
|
152
|
-
const nextLevel = getNextLevel(level, levels);
|
|
140
|
+
const title = generateJobTitle({ discipline, level, track });
|
|
141
|
+
const nextLevel = getNextLevel({ level, levels });
|
|
153
142
|
|
|
154
143
|
// Find other valid tracks for comparison (exclude current track if any)
|
|
155
144
|
const validTracks = tracks.filter(
|
|
@@ -209,7 +198,7 @@ export function prepareProgressDetail({
|
|
|
209
198
|
if (!fromDiscipline || !fromLevel) return null;
|
|
210
199
|
if (!toDiscipline || !toLevel) return null;
|
|
211
200
|
|
|
212
|
-
const fromJob =
|
|
201
|
+
const fromJob = jobCache.getOrCreate({
|
|
213
202
|
discipline: fromDiscipline,
|
|
214
203
|
level: fromLevel,
|
|
215
204
|
track: fromTrack,
|
|
@@ -218,7 +207,7 @@ export function prepareProgressDetail({
|
|
|
218
207
|
capabilities,
|
|
219
208
|
});
|
|
220
209
|
|
|
221
|
-
const toJob =
|
|
210
|
+
const toJob = jobCache.getOrCreate({
|
|
222
211
|
discipline: toDiscipline,
|
|
223
212
|
level: toLevel,
|
|
224
213
|
track: toTrack,
|
|
@@ -95,11 +95,18 @@ export function prepareSkillDetail(
|
|
|
95
95
|
if (!skill) return null;
|
|
96
96
|
|
|
97
97
|
const relatedDisciplines = disciplines
|
|
98
|
-
.filter(
|
|
98
|
+
.filter(
|
|
99
|
+
(d) =>
|
|
100
|
+
getSkillTypeForDiscipline({ discipline: d, skillId: skill.id }) !==
|
|
101
|
+
null,
|
|
102
|
+
)
|
|
99
103
|
.map((d) => ({
|
|
100
104
|
id: d.id,
|
|
101
105
|
name: d.specialization || d.name,
|
|
102
|
-
skillType: getSkillTypeForDiscipline(
|
|
106
|
+
skillType: getSkillTypeForDiscipline({
|
|
107
|
+
discipline: d,
|
|
108
|
+
skillId: skill.id,
|
|
109
|
+
}),
|
|
103
110
|
}));
|
|
104
111
|
|
|
105
112
|
const relatedTracks = tracks
|
|
@@ -88,7 +88,10 @@ export function prepareTrackDetail(track, { skills, behaviours }) {
|
|
|
88
88
|
const skillModifiers = track.skillModifiers
|
|
89
89
|
? Object.entries(track.skillModifiers).map(([key, modifier]) => {
|
|
90
90
|
if (isCapability(key)) {
|
|
91
|
-
const capabilitySkills = getSkillsByCapability(
|
|
91
|
+
const capabilitySkills = getSkillsByCapability({
|
|
92
|
+
skills,
|
|
93
|
+
capability: key,
|
|
94
|
+
});
|
|
92
95
|
return {
|
|
93
96
|
id: key,
|
|
94
97
|
name: key.charAt(0).toUpperCase() + key.slice(1),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent builder install section
|
|
3
|
+
*
|
|
4
|
+
* Surfaces the ecosystem-tool install commands (Microsoft APM and
|
|
5
|
+
* `npx skills`) for the currently selected discipline/track pack. The packs
|
|
6
|
+
* themselves are emitted by `fit-pathway build` when
|
|
7
|
+
* `framework.distribution.siteUrl` is configured — see spec 320 and
|
|
8
|
+
* `products/pathway/src/commands/build-packs.js`. The pack name derivation
|
|
9
|
+
* here must stay in sync with that generator so the command points at an
|
|
10
|
+
* archive that actually exists on the deployed site.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { code, div, h2, p, section } from "../lib/render.js";
|
|
14
|
+
import {
|
|
15
|
+
getDisciplineAbbreviation,
|
|
16
|
+
toKebabCase,
|
|
17
|
+
} from "@forwardimpact/libskill/agent";
|
|
18
|
+
import { createCommandPrompt } from "../components/command-prompt.js";
|
|
19
|
+
|
|
20
|
+
/** Stable id for the install section heading (for aria-labelledby). */
|
|
21
|
+
const INSTALL_HEADING_ID = "agent-builder-install-heading";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derive the pack archive name for a discipline/track combination.
|
|
25
|
+
* Must match `build-packs.js` → `${abbrev}-${toKebabCase(track.id)}`.
|
|
26
|
+
* @param {{id: string}} discipline
|
|
27
|
+
* @param {{id: string}} track
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function getPackName(discipline, track) {
|
|
31
|
+
return `${getDisciplineAbbreviation(discipline.id)}-${toKebabCase(track.id)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalize a site URL by stripping a trailing slash. Matches the
|
|
36
|
+
* normalization applied by `generatePacks` so the displayed URL lines up
|
|
37
|
+
* with the manifest entries.
|
|
38
|
+
* @param {string} siteUrl
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function normalizeSiteUrl(siteUrl) {
|
|
42
|
+
return siteUrl.replace(/\/$/, "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the `apm install` command for a specific pack archive. Targets the
|
|
47
|
+
* direct archive URL (rather than a registry-style shorthand) because it is
|
|
48
|
+
* the most durable path through APM's evolving resolution logic and matches
|
|
49
|
+
* the URL listed in the generated `apm.yml` manifest.
|
|
50
|
+
* @param {string} siteUrl
|
|
51
|
+
* @param {string} packName
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function getApmInstallCommand(siteUrl, packName) {
|
|
55
|
+
return `apm install ${normalizeSiteUrl(siteUrl)}/packs/${packName}.tar.gz`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the `npx skills add` command that discovers the published pack
|
|
60
|
+
* registry at `<siteUrl>/.well-known/agent-skills/index.json`.
|
|
61
|
+
* @param {string} siteUrl
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function getSkillsAddCommand(siteUrl) {
|
|
65
|
+
return `npx skills add ${normalizeSiteUrl(siteUrl)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render the install section for the selected agent combination. Returns
|
|
70
|
+
* `null` when no site URL is configured (no packs have been published, so
|
|
71
|
+
* there is nothing meaningful to install) so the caller can skip rendering.
|
|
72
|
+
* @param {Object} params
|
|
73
|
+
* @param {{id: string}} params.discipline - Selected human discipline
|
|
74
|
+
* @param {{id: string}} params.track - Selected human track
|
|
75
|
+
* @param {string|undefined} params.siteUrl - Framework distribution site URL
|
|
76
|
+
* @returns {HTMLElement|null}
|
|
77
|
+
*/
|
|
78
|
+
export function createInstallSection({ discipline, track, siteUrl }) {
|
|
79
|
+
if (!siteUrl) return null;
|
|
80
|
+
|
|
81
|
+
const packName = getPackName(discipline, track);
|
|
82
|
+
const apmCommand = getApmInstallCommand(siteUrl, packName);
|
|
83
|
+
const skillsCommand = getSkillsAddCommand(siteUrl);
|
|
84
|
+
|
|
85
|
+
return section(
|
|
86
|
+
{
|
|
87
|
+
className: "agent-install-section",
|
|
88
|
+
"aria-labelledby": INSTALL_HEADING_ID,
|
|
89
|
+
},
|
|
90
|
+
div(
|
|
91
|
+
{ className: "agent-install-header" },
|
|
92
|
+
h2({ id: INSTALL_HEADING_ID }, "📦 Install This Agent Team"),
|
|
93
|
+
p(
|
|
94
|
+
{ className: "text-muted agent-install-description" },
|
|
95
|
+
"Install the pre-built pack for this discipline × track combination " +
|
|
96
|
+
"directly through an ecosystem package manager. The pack contains " +
|
|
97
|
+
"the same stage agents, skills, team instructions, and Claude Code " +
|
|
98
|
+
"settings shown below — installed into your project's ",
|
|
99
|
+
code({}, ".claude/"),
|
|
100
|
+
" directory.",
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
div(
|
|
104
|
+
{ className: "agent-install-commands" },
|
|
105
|
+
div(
|
|
106
|
+
{ className: "agent-install-command" },
|
|
107
|
+
p({ className: "agent-install-command-label" }, "Microsoft APM"),
|
|
108
|
+
createCommandPrompt(apmCommand),
|
|
109
|
+
),
|
|
110
|
+
div(
|
|
111
|
+
{ className: "agent-install-command" },
|
|
112
|
+
p({ className: "agent-install-command-label" }, "npx skills"),
|
|
113
|
+
createCommandPrompt(skillsCommand),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
deriveStageAgent,
|
|
11
11
|
generateSkillMarkdown,
|
|
12
12
|
deriveAgentSkills,
|
|
13
|
-
|
|
14
|
-
} from "@forwardimpact/libskill";
|
|
13
|
+
} from "@forwardimpact/libskill/agent";
|
|
14
|
+
import { deriveToolkit } from "@forwardimpact/libskill/toolkit";
|
|
15
15
|
import { getStageEmoji } from "../formatters/stage/shared.js";
|
|
16
16
|
import { formatAgentProfile } from "../formatters/agent/profile.js";
|
|
17
17
|
import {
|
|
@@ -115,7 +115,7 @@ function deriveSkillData(context) {
|
|
|
115
115
|
const skillFiles = derivedSkills
|
|
116
116
|
.map((d) => skills.find((s) => s.id === d.skillId))
|
|
117
117
|
.filter((skill) => skill?.agent)
|
|
118
|
-
.map((skill) => generateSkillMarkdown(skill, stages));
|
|
118
|
+
.map((skill) => generateSkillMarkdown({ skillData: skill, stages }));
|
|
119
119
|
|
|
120
120
|
const toolkit = deriveToolkit({
|
|
121
121
|
skillMatrix: derivedSkills,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "../lib/render.js";
|
|
19
19
|
import { getState } from "../lib/state.js";
|
|
20
20
|
import { loadAgentDataBrowser } from "../lib/yaml-loader.js";
|
|
21
|
-
import { deriveReferenceLevel } from "@forwardimpact/libskill";
|
|
21
|
+
import { deriveReferenceLevel } from "@forwardimpact/libskill/agent";
|
|
22
22
|
import {
|
|
23
23
|
createSelectWithValue,
|
|
24
24
|
createDisciplineSelect,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
createSingleStagePreview,
|
|
31
31
|
createHelpSection,
|
|
32
32
|
} from "./agent-builder-preview.js";
|
|
33
|
+
import { createInstallSection } from "./agent-builder-install.js";
|
|
33
34
|
|
|
34
35
|
/** All stages option value */
|
|
35
36
|
const ALL_STAGES_VALUE = "all";
|
|
@@ -79,6 +80,7 @@ async function getTemplates() {
|
|
|
79
80
|
*/
|
|
80
81
|
export async function renderAgentBuilder() {
|
|
81
82
|
const { data } = getState();
|
|
83
|
+
const siteUrl = data.framework?.distribution?.siteUrl;
|
|
82
84
|
|
|
83
85
|
// Show loading state
|
|
84
86
|
render(
|
|
@@ -260,8 +262,27 @@ export async function renderAgentBuilder() {
|
|
|
260
262
|
templates,
|
|
261
263
|
};
|
|
262
264
|
|
|
265
|
+
// Install section (ecosystem-tool install commands) — appears above the
|
|
266
|
+
// preview cards so the install action is visible before users scroll
|
|
267
|
+
// through the generated files. Only rendered when the framework has a
|
|
268
|
+
// published distribution site URL, since the packs only exist at that
|
|
269
|
+
// URL after a `fit-pathway build`. Must come after the stage-validity
|
|
270
|
+
// guard below so an invalid stage id (e.g. from a stale bookmark) does
|
|
271
|
+
// not pair the install card with a "Stage not found" error.
|
|
272
|
+
function appendInstallSection() {
|
|
273
|
+
const installSection = createInstallSection({
|
|
274
|
+
discipline: humanDiscipline,
|
|
275
|
+
track: humanTrack,
|
|
276
|
+
siteUrl,
|
|
277
|
+
});
|
|
278
|
+
if (installSection) {
|
|
279
|
+
previewContainer.appendChild(installSection);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
263
283
|
// Generate preview based on stage selection
|
|
264
284
|
if (stage === ALL_STAGES_VALUE) {
|
|
285
|
+
appendInstallSection();
|
|
265
286
|
previewContainer.appendChild(createAllStagesPreview(context));
|
|
266
287
|
} else {
|
|
267
288
|
const stageObj = stages.find((s) => s.id === stage);
|
|
@@ -274,6 +295,7 @@ export async function renderAgentBuilder() {
|
|
|
274
295
|
);
|
|
275
296
|
return;
|
|
276
297
|
}
|
|
298
|
+
appendInstallSection();
|
|
277
299
|
previewContainer.appendChild(createSingleStagePreview(context, stageObj));
|
|
278
300
|
}
|
|
279
301
|
}
|
package/src/pages/progress.js
CHANGED
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
prepareCurrentJob,
|
|
17
17
|
prepareCustomProgression,
|
|
18
18
|
getDefaultTargetLevel,
|
|
19
|
-
isValidCombination,
|
|
20
19
|
} from "../formatters/progress/shared.js";
|
|
20
|
+
import { isValidJobCombination } from "@forwardimpact/libskill/derivation";
|
|
21
21
|
import { buildComparisonResult } from "./progress-comparison.js";
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -188,7 +188,7 @@ function createComparisonSelectorsSection({
|
|
|
188
188
|
for (const level of data.levels) {
|
|
189
189
|
// Check trackless combination
|
|
190
190
|
if (
|
|
191
|
-
|
|
191
|
+
isValidJobCombination({ discipline: selectedDisc, level, track: null })
|
|
192
192
|
) {
|
|
193
193
|
if (!validLevels.find((g) => g.id === level.id)) {
|
|
194
194
|
validLevels.push(level);
|
|
@@ -197,7 +197,7 @@ function createComparisonSelectorsSection({
|
|
|
197
197
|
}
|
|
198
198
|
// Check each track combination
|
|
199
199
|
for (const track of data.tracks) {
|
|
200
|
-
if (
|
|
200
|
+
if (isValidJobCombination({ discipline: selectedDisc, level, track })) {
|
|
201
201
|
if (!validLevels.find((g) => g.id === level.id)) {
|
|
202
202
|
validLevels.push(level);
|
|
203
203
|
}
|
package/src/pages/skill.js
CHANGED
|
@@ -11,7 +11,7 @@ import { prepareSkillsList } from "../formatters/skill/shared.js";
|
|
|
11
11
|
import { skillToDOM } from "../formatters/skill/dom.js";
|
|
12
12
|
import { skillToCardConfig } from "../lib/card-mappers.js";
|
|
13
13
|
import { getCapabilityEmoji, getConceptEmoji } from "@forwardimpact/map/levels";
|
|
14
|
-
import { generateSkillMarkdown } from "@forwardimpact/libskill";
|
|
14
|
+
import { generateSkillMarkdown } from "@forwardimpact/libskill/agent";
|
|
15
15
|
import { formatAgentSkill } from "../formatters/agent/skill.js";
|
|
16
16
|
|
|
17
17
|
/** @type {string|null} Cached skill template */
|
|
@@ -101,7 +101,10 @@ export async function renderSkillDetail(params) {
|
|
|
101
101
|
let agentSkillContent;
|
|
102
102
|
if (skill.agent) {
|
|
103
103
|
const template = await getSkillTemplate();
|
|
104
|
-
const skillData = generateSkillMarkdown(
|
|
104
|
+
const skillData = generateSkillMarkdown({
|
|
105
|
+
skillData: skill,
|
|
106
|
+
stages: data.stages,
|
|
107
|
+
});
|
|
105
108
|
agentSkillContent = formatAgentSkill(skillData, template);
|
|
106
109
|
}
|
|
107
110
|
|
package/src/lib/job-cache.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Job Cache
|
|
3
|
-
*
|
|
4
|
-
* Centralized caching for generated job definitions.
|
|
5
|
-
* Provides consistent key generation and get-or-create pattern.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { deriveJob } from "@forwardimpact/libskill/derivation";
|
|
9
|
-
|
|
10
|
-
/** @type {Map<string, Object>} */
|
|
11
|
-
const cache = new Map();
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Build a consistent cache key from job parameters
|
|
15
|
-
* @param {string} disciplineId
|
|
16
|
-
* @param {string} levelId
|
|
17
|
-
* @param {string} [trackId] - Optional track ID
|
|
18
|
-
* @returns {string}
|
|
19
|
-
*/
|
|
20
|
-
export function buildJobKey(disciplineId, levelId, trackId = null) {
|
|
21
|
-
if (trackId) {
|
|
22
|
-
return `${disciplineId}_${levelId}_${trackId}`;
|
|
23
|
-
}
|
|
24
|
-
return `${disciplineId}_${levelId}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get or create a cached job definition
|
|
29
|
-
* @param {Object} params
|
|
30
|
-
* @param {Object} params.discipline
|
|
31
|
-
* @param {Object} params.level
|
|
32
|
-
* @param {Object} [params.track] - Optional track
|
|
33
|
-
* @param {Array} params.skills
|
|
34
|
-
* @param {Array} params.behaviours
|
|
35
|
-
* @param {Array} [params.capabilities]
|
|
36
|
-
* @returns {Object|null}
|
|
37
|
-
*/
|
|
38
|
-
export function getOrCreateJob({
|
|
39
|
-
discipline,
|
|
40
|
-
level,
|
|
41
|
-
track = null,
|
|
42
|
-
skills,
|
|
43
|
-
behaviours,
|
|
44
|
-
capabilities,
|
|
45
|
-
}) {
|
|
46
|
-
const key = buildJobKey(discipline.id, level.id, track?.id);
|
|
47
|
-
|
|
48
|
-
if (!cache.has(key)) {
|
|
49
|
-
const job = deriveJob({
|
|
50
|
-
discipline,
|
|
51
|
-
level,
|
|
52
|
-
track,
|
|
53
|
-
skills,
|
|
54
|
-
behaviours,
|
|
55
|
-
capabilities,
|
|
56
|
-
});
|
|
57
|
-
if (job) {
|
|
58
|
-
cache.set(key, job);
|
|
59
|
-
}
|
|
60
|
-
return job;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return cache.get(key);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Clear all cached jobs
|
|
68
|
-
*/
|
|
69
|
-
export function clearCache() {
|
|
70
|
-
cache.clear();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Invalidate a specific job from the cache
|
|
75
|
-
* @param {string} disciplineId
|
|
76
|
-
* @param {string} levelId
|
|
77
|
-
* @param {string} [trackId] - Optional track ID
|
|
78
|
-
*/
|
|
79
|
-
export function invalidateCachedJob(disciplineId, levelId, trackId = null) {
|
|
80
|
-
cache.delete(buildJobKey(disciplineId, levelId, trackId));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get the number of cached jobs (for testing/debugging)
|
|
85
|
-
* @returns {number}
|
|
86
|
-
*/
|
|
87
|
-
export function getCachedJobCount() {
|
|
88
|
-
return cache.size;
|
|
89
|
-
}
|