@forwardimpact/pathway 0.3.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 +1 -1
- package/app/commands/index.js +4 -3
- package/app/commands/skill.js +56 -2
- package/app/commands/tool.js +112 -0
- 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 -9
- package/app/formatters/agent/skill.js +48 -6
- package/app/formatters/job/description.js +21 -16
- package/app/formatters/job/dom.js +9 -70
- package/app/formatters/shared.js +58 -0
- 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/index.html +10 -3
- package/app/lib/card-mappers.js +64 -17
- package/app/lib/render.js +4 -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 +2 -2
- package/app/pages/landing.js +34 -14
- package/app/pages/self-assessment.js +7 -5
- 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/bin/pathway.js +31 -16
- 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 +5 -7
- package/examples/self-assessments.yaml +1 -1
- package/package.json +1 -1
- package/templates/skill.template.md +31 -12
package/app/commands/agent.js
CHANGED
|
@@ -454,7 +454,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
|
|
|
454
454
|
const skillFiles = derivedSkills
|
|
455
455
|
.map((derived) => skillsWithAgent.find((s) => s.id === derived.skillId))
|
|
456
456
|
.filter((skill) => skill?.agent)
|
|
457
|
-
.map((skill) => generateSkillMd(skill));
|
|
457
|
+
.map((skill) => generateSkillMd(skill, data.stages));
|
|
458
458
|
|
|
459
459
|
// Validate all profiles
|
|
460
460
|
for (const profile of profiles) {
|
package/app/commands/index.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Re-exports all command handlers for convenient importing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { runSkillCommand } from "./skill.js";
|
|
8
|
-
export { runBehaviourCommand } from "./behaviour.js";
|
|
9
|
-
export { runDriverCommand } from "./driver.js";
|
|
10
7
|
export { runDisciplineCommand } from "./discipline.js";
|
|
11
8
|
export { runGradeCommand } from "./grade.js";
|
|
12
9
|
export { runTrackCommand } from "./track.js";
|
|
10
|
+
export { runBehaviourCommand } from "./behaviour.js";
|
|
11
|
+
export { runSkillCommand } from "./skill.js";
|
|
12
|
+
export { runDriverCommand } from "./driver.js";
|
|
13
13
|
export { runStageCommand } from "./stage.js";
|
|
14
|
+
export { runToolCommand } from "./tool.js";
|
|
14
15
|
export { runJobCommand } from "./job.js";
|
|
15
16
|
export { runInterviewCommand } from "./interview.js";
|
|
16
17
|
export { runProgressCommand } from "./progress.js";
|
package/app/commands/skill.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* npx pathway skill # Summary with stats
|
|
8
8
|
* npx pathway skill --list # IDs only (for piping)
|
|
9
9
|
* npx pathway skill <id> # Detail view
|
|
10
|
+
* npx pathway skill <id> --agent # Agent SKILL.md output
|
|
10
11
|
* npx pathway skill --validate # Validation checks
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -14,7 +15,10 @@ import { createEntityCommand } from "./command-factory.js";
|
|
|
14
15
|
import { skillToMarkdown } from "../formatters/skill/markdown.js";
|
|
15
16
|
import { prepareSkillsList } from "../formatters/skill/shared.js";
|
|
16
17
|
import { getConceptEmoji } from "../model/levels.js";
|
|
17
|
-
import { formatTable } from "../lib/cli-output.js";
|
|
18
|
+
import { formatTable, formatError } from "../lib/cli-output.js";
|
|
19
|
+
import { generateSkillMd } from "../model/agent.js";
|
|
20
|
+
import { formatAgentSkill } from "../formatters/agent/skill.js";
|
|
21
|
+
import { loadSkillTemplate } from "../lib/template-loader.js";
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Format skill summary output
|
|
@@ -59,7 +63,29 @@ function formatDetail(viewAndContext, framework) {
|
|
|
59
63
|
);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Format skill as agent SKILL.md output
|
|
68
|
+
* @param {Object} skill - Skill entity with agent section
|
|
69
|
+
* @param {Array} stages - All stage entities
|
|
70
|
+
* @param {string} dataDir - Path to data directory for template loading
|
|
71
|
+
*/
|
|
72
|
+
async function formatAgentDetail(skill, stages, dataDir) {
|
|
73
|
+
if (!skill.agent) {
|
|
74
|
+
console.error(formatError(`Skill '${skill.id}' has no agent section`));
|
|
75
|
+
console.error(`\nSkills with agent support:`);
|
|
76
|
+
console.error(
|
|
77
|
+
` npx pathway skill --list | xargs -I{} sh -c 'npx pathway skill {} --json | jq -e .skill.agent > /dev/null && echo {}'`,
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const template = await loadSkillTemplate(dataDir);
|
|
83
|
+
const skillMd = generateSkillMd(skill, stages);
|
|
84
|
+
const output = formatAgentSkill(skillMd, template);
|
|
85
|
+
console.log(output);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const baseSkillCommand = createEntityCommand({
|
|
63
89
|
entityName: "skill",
|
|
64
90
|
pluralName: "skills",
|
|
65
91
|
findEntity: (data, id) => data.skills.find((s) => s.id === id),
|
|
@@ -74,3 +100,31 @@ export const runSkillCommand = createEntityCommand({
|
|
|
74
100
|
formatDetail,
|
|
75
101
|
emoji: "📚",
|
|
76
102
|
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run skill command with --agent support
|
|
106
|
+
* @param {Object} params - Command parameters
|
|
107
|
+
* @param {Object} params.data - Loaded pathway data
|
|
108
|
+
* @param {string[]} params.args - Command arguments
|
|
109
|
+
* @param {Object} params.options - Command options
|
|
110
|
+
* @param {string} params.dataDir - Path to data directory
|
|
111
|
+
*/
|
|
112
|
+
export async function runSkillCommand({ data, args, options, dataDir }) {
|
|
113
|
+
// Handle --agent flag for detail view
|
|
114
|
+
if (options.agent && args.length > 0) {
|
|
115
|
+
const [id] = args;
|
|
116
|
+
const skill = data.skills.find((s) => s.id === id);
|
|
117
|
+
|
|
118
|
+
if (!skill) {
|
|
119
|
+
console.error(formatError(`Skill not found: ${id}`));
|
|
120
|
+
console.error(`Available: ${data.skills.map((s) => s.id).join(", ")}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await formatAgentDetail(skill, data.stages, dataDir);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Delegate to base command for all other cases
|
|
129
|
+
return baseSkillCommand({ data, args, options, dataDir });
|
|
130
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Handles tool summary, listing, and detail display in the terminal.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx pathway tool # Summary with stats
|
|
8
|
+
* npx pathway tool --list # Tool names only (for piping)
|
|
9
|
+
* npx pathway tool <name> # Detail view for specific tool
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { prepareToolsList } from "../formatters/tool/shared.js";
|
|
13
|
+
import {
|
|
14
|
+
formatTable,
|
|
15
|
+
formatHeader,
|
|
16
|
+
formatSubheader,
|
|
17
|
+
} from "../lib/cli-output.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run tool command
|
|
21
|
+
* @param {Object} params - Command parameters
|
|
22
|
+
* @param {Object} params.data - Loaded pathway data
|
|
23
|
+
* @param {string[]} params.args - Command arguments
|
|
24
|
+
* @param {Object} params.options - Command options
|
|
25
|
+
*/
|
|
26
|
+
export async function runToolCommand({ data, args, options }) {
|
|
27
|
+
const [name] = args;
|
|
28
|
+
const { tools, totalCount } = prepareToolsList(data.skills);
|
|
29
|
+
|
|
30
|
+
// --list: Output clean newline-separated tool names for piping
|
|
31
|
+
if (options.list) {
|
|
32
|
+
for (const tool of tools) {
|
|
33
|
+
console.log(tool.name);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// No args: Show summary
|
|
39
|
+
if (!name) {
|
|
40
|
+
if (options.json) {
|
|
41
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
formatSummary(tools, totalCount);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// With name: Show detail
|
|
49
|
+
const tool = tools.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
50
|
+
|
|
51
|
+
if (!tool) {
|
|
52
|
+
console.error(`Tool not found: ${name}`);
|
|
53
|
+
console.error(`Available: ${tools.map((t) => t.name).join(", ")}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.json) {
|
|
58
|
+
console.log(JSON.stringify(tool, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
formatDetail(tool);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format tool summary output
|
|
67
|
+
* @param {Array} tools - Aggregated tools
|
|
68
|
+
* @param {number} totalCount - Total tool count
|
|
69
|
+
*/
|
|
70
|
+
function formatSummary(tools, totalCount) {
|
|
71
|
+
console.log(`\n🔧 Tools\n`);
|
|
72
|
+
|
|
73
|
+
// Show tools sorted by usage count
|
|
74
|
+
const sorted = [...tools].sort((a, b) => b.usages.length - a.usages.length);
|
|
75
|
+
const rows = sorted
|
|
76
|
+
.slice(0, 15)
|
|
77
|
+
.map((t) => [
|
|
78
|
+
t.name,
|
|
79
|
+
t.usages.length,
|
|
80
|
+
t.description.length > 50
|
|
81
|
+
? t.description.slice(0, 47) + "..."
|
|
82
|
+
: t.description,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
console.log(formatTable(["Tool", "Skills", "Description"], rows));
|
|
86
|
+
console.log(`\nTotal: ${totalCount} tools`);
|
|
87
|
+
if (sorted.length > 15) {
|
|
88
|
+
console.log(`(showing top 15 by usage)`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`\nRun 'npx pathway tool --list' for all tool names`);
|
|
91
|
+
console.log(`Run 'npx pathway tool <name>' for details\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Format tool detail output
|
|
96
|
+
* @param {Object} tool - Aggregated tool with usages
|
|
97
|
+
*/
|
|
98
|
+
function formatDetail(tool) {
|
|
99
|
+
console.log(formatHeader(`\n🔧 ${tool.name}\n`));
|
|
100
|
+
console.log(`${tool.description}\n`);
|
|
101
|
+
|
|
102
|
+
if (tool.url) {
|
|
103
|
+
console.log(`Documentation: ${tool.url}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (tool.usages.length > 0) {
|
|
107
|
+
console.log(formatSubheader("Used in Skills\n"));
|
|
108
|
+
const rows = tool.usages.map((u) => [u.skillName, u.useWhen]);
|
|
109
|
+
console.log(formatTable(["Skill", "Use When"], rows));
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -43,7 +43,7 @@ export function createChecklist(checklist, options = {}) {
|
|
|
43
43
|
function createChecklistGroup(group, options) {
|
|
44
44
|
const { interactive, capabilities } = options;
|
|
45
45
|
const emoji = getCapabilityEmoji(capabilities, group.capability);
|
|
46
|
-
const capabilityName = formatCapabilityName(group.capability);
|
|
46
|
+
const capabilityName = formatCapabilityName(group.capability, capabilities);
|
|
47
47
|
|
|
48
48
|
return div(
|
|
49
49
|
{ className: "checklist-group" },
|
|
@@ -90,11 +90,13 @@ function createInteractiveCheckbox() {
|
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Format capability name for display
|
|
93
|
-
* @param {string}
|
|
93
|
+
* @param {string} capabilityId - Capability ID
|
|
94
|
+
* @param {Array} capabilities - Capabilities array
|
|
94
95
|
* @returns {string}
|
|
95
96
|
*/
|
|
96
|
-
function formatCapabilityName(
|
|
97
|
-
|
|
97
|
+
function formatCapabilityName(capabilityId, capabilities) {
|
|
98
|
+
const capability = capabilities.find((c) => c.id === capabilityId);
|
|
99
|
+
return capability?.name || capabilityId;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/**
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Textarea Component
|
|
3
|
+
*
|
|
4
|
+
* Reusable read-only textarea with copy buttons for displaying markdown content.
|
|
5
|
+
* Used by job descriptions and skill implementation patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* global Prism */
|
|
9
|
+
import { div, p, button } from "../lib/render.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a copy button that copies content to clipboard
|
|
13
|
+
* @param {string} content - The text content to copy
|
|
14
|
+
* @param {string} label - Button label text
|
|
15
|
+
* @param {string} [className="btn btn-primary"] - Button class
|
|
16
|
+
* @returns {HTMLElement}
|
|
17
|
+
*/
|
|
18
|
+
export function createCopyButton(
|
|
19
|
+
content,
|
|
20
|
+
label,
|
|
21
|
+
className = "btn btn-primary",
|
|
22
|
+
) {
|
|
23
|
+
const btn = button(
|
|
24
|
+
{
|
|
25
|
+
className: `${className} copy-btn`,
|
|
26
|
+
onClick: async () => {
|
|
27
|
+
try {
|
|
28
|
+
await navigator.clipboard.writeText(content);
|
|
29
|
+
btn.textContent = "✓ Copied!";
|
|
30
|
+
btn.classList.add("copied");
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
btn.textContent = label;
|
|
33
|
+
btn.classList.remove("copied");
|
|
34
|
+
}, 2000);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error("Failed to copy:", err);
|
|
37
|
+
btn.textContent = "Copy failed";
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
btn.textContent = label;
|
|
40
|
+
}, 2000);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
label,
|
|
45
|
+
);
|
|
46
|
+
return btn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a copy button that copies HTML to clipboard (for rich text pasting)
|
|
51
|
+
* @param {string} html - The HTML content to copy
|
|
52
|
+
* @param {string} label - Button label text
|
|
53
|
+
* @returns {HTMLElement}
|
|
54
|
+
*/
|
|
55
|
+
export function createCopyHtmlButton(html, label) {
|
|
56
|
+
const btn = button(
|
|
57
|
+
{
|
|
58
|
+
className: "btn btn-secondary copy-btn",
|
|
59
|
+
onClick: async () => {
|
|
60
|
+
try {
|
|
61
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
62
|
+
const clipboardItem = new ClipboardItem({ "text/html": blob });
|
|
63
|
+
await navigator.clipboard.write([clipboardItem]);
|
|
64
|
+
btn.textContent = "✓ Copied!";
|
|
65
|
+
btn.classList.add("copied");
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
btn.textContent = label;
|
|
68
|
+
btn.classList.remove("copied");
|
|
69
|
+
}, 2000);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error("Failed to copy:", err);
|
|
72
|
+
btn.textContent = "Copy failed";
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
btn.textContent = label;
|
|
75
|
+
}, 2000);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
label,
|
|
80
|
+
);
|
|
81
|
+
return btn;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a markdown textarea with copy buttons
|
|
86
|
+
* @param {Object} options
|
|
87
|
+
* @param {string} options.markdown - The markdown content to display
|
|
88
|
+
* @param {string} [options.description] - Optional description text above the textarea
|
|
89
|
+
* @param {string} [options.copyLabel="Copy Markdown"] - Label for the copy button
|
|
90
|
+
* @param {Function} [options.toHtml] - Optional function to convert markdown to HTML for rich copy
|
|
91
|
+
* @param {string} [options.copyHtmlLabel="Copy as HTML"] - Label for the HTML copy button
|
|
92
|
+
* @param {number} [options.minHeight=300] - Minimum height in pixels
|
|
93
|
+
* @returns {HTMLElement}
|
|
94
|
+
*/
|
|
95
|
+
export function createMarkdownTextarea({
|
|
96
|
+
markdown,
|
|
97
|
+
description,
|
|
98
|
+
copyLabel = "Copy Markdown",
|
|
99
|
+
toHtml,
|
|
100
|
+
copyHtmlLabel = "Copy as HTML",
|
|
101
|
+
minHeight = 300,
|
|
102
|
+
}) {
|
|
103
|
+
// Create highlighted code block
|
|
104
|
+
const pre = document.createElement("pre");
|
|
105
|
+
pre.className = "markdown-display";
|
|
106
|
+
pre.style.minHeight = `${minHeight}px`;
|
|
107
|
+
|
|
108
|
+
const code = document.createElement("code");
|
|
109
|
+
code.className = "language-markdown";
|
|
110
|
+
code.textContent = markdown;
|
|
111
|
+
pre.appendChild(code);
|
|
112
|
+
|
|
113
|
+
// Apply Prism highlighting if available
|
|
114
|
+
if (typeof Prism !== "undefined") {
|
|
115
|
+
Prism.highlightElement(code);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const buttons = [createCopyButton(markdown, copyLabel)];
|
|
119
|
+
if (toHtml) {
|
|
120
|
+
buttons.push(createCopyHtmlButton(toHtml(markdown), copyHtmlLabel));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return div(
|
|
124
|
+
{ className: "markdown-textarea-container" },
|
|
125
|
+
div(
|
|
126
|
+
{ className: "markdown-textarea-header" },
|
|
127
|
+
description ? p({ className: "text-muted" }, description) : null,
|
|
128
|
+
div({ className: "button-group" }, ...buttons),
|
|
129
|
+
),
|
|
130
|
+
pre,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -102,4 +102,49 @@
|
|
|
102
102
|
background: var(--color-primary-light, rgba(37, 99, 235, 0.1));
|
|
103
103
|
color: var(--color-primary);
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
/* Markdown display - read-only code block with copy buttons */
|
|
107
|
+
.markdown-textarea-container {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: var(--space-md);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.markdown-textarea-header {
|
|
114
|
+
display: flex;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
align-items: center;
|
|
117
|
+
flex-wrap: wrap;
|
|
118
|
+
gap: var(--space-md);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.markdown-textarea-header .text-muted {
|
|
122
|
+
margin: 0;
|
|
123
|
+
flex: 1;
|
|
124
|
+
min-width: 200px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.markdown-display {
|
|
128
|
+
width: 100%;
|
|
129
|
+
margin: 0;
|
|
130
|
+
padding: var(--space-md);
|
|
131
|
+
font-family:
|
|
132
|
+
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
133
|
+
font-size: var(--font-size-sm);
|
|
134
|
+
line-height: 1.6;
|
|
135
|
+
background-color: var(--color-surface) !important;
|
|
136
|
+
border: 1px solid var(--color-border);
|
|
137
|
+
border-radius: var(--radius-md);
|
|
138
|
+
overflow: auto;
|
|
139
|
+
color: var(--color-text);
|
|
140
|
+
white-space: pre-wrap;
|
|
141
|
+
word-wrap: break-word;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.markdown-display code {
|
|
145
|
+
background: transparent;
|
|
146
|
+
padding: 0;
|
|
147
|
+
font-size: inherit;
|
|
148
|
+
color: inherit;
|
|
149
|
+
}
|
|
105
150
|
}
|
|
@@ -95,6 +95,10 @@
|
|
|
95
95
|
grid-template-columns: repeat(3, 1fr);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
.grid-4 {
|
|
99
|
+
grid-template-columns: repeat(4, 1fr);
|
|
100
|
+
}
|
|
101
|
+
|
|
98
102
|
.grid-6 {
|
|
99
103
|
grid-template-columns: repeat(6, 1fr);
|
|
100
104
|
}
|
|
@@ -174,6 +178,10 @@
|
|
|
174
178
|
grid-template-columns: repeat(2, 1fr);
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
.grid-4 {
|
|
182
|
+
grid-template-columns: repeat(2, 1fr);
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
.grid-6 {
|
|
178
186
|
grid-template-columns: repeat(3, 1fr);
|
|
179
187
|
}
|
|
@@ -198,6 +206,10 @@
|
|
|
198
206
|
grid-template-columns: 1fr;
|
|
199
207
|
}
|
|
200
208
|
|
|
209
|
+
.grid-4 {
|
|
210
|
+
grid-template-columns: repeat(2, 1fr);
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
.grid-6 {
|
|
202
214
|
grid-template-columns: repeat(2, 1fr);
|
|
203
215
|
}
|
|
@@ -53,6 +53,28 @@
|
|
|
53
53
|
margin-top: var(--space-md);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/* Tool skills list (inside cards) */
|
|
57
|
+
.tool-skills-list {
|
|
58
|
+
list-style: none;
|
|
59
|
+
padding: 0;
|
|
60
|
+
margin: var(--space-sm) 0 0 0;
|
|
61
|
+
font-size: var(--font-size-sm);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.tool-skills-list li {
|
|
65
|
+
padding: var(--space-xs) 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.tool-skills-list a {
|
|
69
|
+
color: var(--color-text-secondary);
|
|
70
|
+
text-decoration: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tool-skills-list a:hover {
|
|
74
|
+
color: var(--color-primary);
|
|
75
|
+
text-decoration: underline;
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
/* Stat cards */
|
|
57
79
|
.stat-card {
|
|
58
80
|
background: var(--color-surface);
|
package/app/css/pages/detail.css
CHANGED
|
@@ -56,4 +56,54 @@
|
|
|
56
56
|
align-items: center;
|
|
57
57
|
gap: var(--space-xs);
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/* Tools table (for skill detail page) */
|
|
61
|
+
.tools-table {
|
|
62
|
+
width: 100%;
|
|
63
|
+
border-collapse: separate;
|
|
64
|
+
border-spacing: 0;
|
|
65
|
+
margin-top: var(--space-md);
|
|
66
|
+
border: 1px solid var(--color-border);
|
|
67
|
+
border-radius: var(--radius-lg);
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
background: var(--color-surface);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.tools-table th,
|
|
73
|
+
.tools-table td {
|
|
74
|
+
padding: var(--space-md) var(--space-lg);
|
|
75
|
+
text-align: left;
|
|
76
|
+
border-bottom: 1px solid var(--color-border);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.tools-table th {
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
font-size: var(--font-size-sm);
|
|
82
|
+
color: var(--color-text-secondary);
|
|
83
|
+
background: var(--color-bg);
|
|
84
|
+
text-transform: uppercase;
|
|
85
|
+
letter-spacing: 0.025em;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.tools-table th:first-child {
|
|
89
|
+
width: 200px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.tools-table tbody tr:last-child td {
|
|
93
|
+
border-bottom: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.tools-table tbody tr:hover {
|
|
97
|
+
background: var(--color-bg);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* See all link */
|
|
101
|
+
.see-all-link {
|
|
102
|
+
margin-top: var(--space-md);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.see-all-link a {
|
|
106
|
+
color: var(--color-primary);
|
|
107
|
+
font-size: var(--font-size-sm);
|
|
108
|
+
}
|
|
59
109
|
}
|
|
@@ -89,46 +89,4 @@
|
|
|
89
89
|
font-weight: 600;
|
|
90
90
|
color: var(--color-primary);
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
/* Job description section */
|
|
94
|
-
.job-description-container {
|
|
95
|
-
display: flex;
|
|
96
|
-
flex-direction: column;
|
|
97
|
-
gap: var(--space-md);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.job-description-header {
|
|
101
|
-
display: flex;
|
|
102
|
-
justify-content: space-between;
|
|
103
|
-
align-items: center;
|
|
104
|
-
flex-wrap: wrap;
|
|
105
|
-
gap: var(--space-md);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.job-description-header .text-muted {
|
|
109
|
-
margin: 0;
|
|
110
|
-
flex: 1;
|
|
111
|
-
min-width: 200px;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.job-description-textarea {
|
|
115
|
-
width: 100%;
|
|
116
|
-
min-height: 400px;
|
|
117
|
-
padding: var(--space-md);
|
|
118
|
-
font-family:
|
|
119
|
-
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
120
|
-
font-size: var(--font-size-sm);
|
|
121
|
-
line-height: 1.6;
|
|
122
|
-
background-color: var(--color-bg);
|
|
123
|
-
border: 1px solid var(--color-border);
|
|
124
|
-
border-radius: var(--radius-md);
|
|
125
|
-
resize: vertical;
|
|
126
|
-
color: var(--color-text);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.job-description-textarea:focus {
|
|
130
|
-
outline: none;
|
|
131
|
-
border-color: var(--color-primary);
|
|
132
|
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
133
|
-
}
|
|
134
92
|
}
|
|
@@ -10,6 +10,66 @@
|
|
|
10
10
|
|
|
11
11
|
import Mustache from "mustache";
|
|
12
12
|
|
|
13
|
+
import { trimValue, trimRequired, trimFields } from "../shared.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prepare agent profile data for template rendering
|
|
17
|
+
* Normalizes string values by trimming trailing newlines for consistent template output.
|
|
18
|
+
* @param {Object} params
|
|
19
|
+
* @param {Object} params.frontmatter - YAML frontmatter data
|
|
20
|
+
* @param {string} params.frontmatter.name - Agent name
|
|
21
|
+
* @param {string} params.frontmatter.description - Agent description
|
|
22
|
+
* @param {boolean} params.frontmatter.infer - Whether to auto-select
|
|
23
|
+
* @param {Array} [params.frontmatter.handoffs] - Handoff definitions
|
|
24
|
+
* @param {Object} params.bodyData - Structured body data
|
|
25
|
+
* @param {string} params.bodyData.title - Agent title
|
|
26
|
+
* @param {string} params.bodyData.stageDescription - Stage description text
|
|
27
|
+
* @param {string} params.bodyData.identity - Core identity text
|
|
28
|
+
* @param {string} [params.bodyData.priority] - Priority/philosophy statement
|
|
29
|
+
* @param {string[]} params.bodyData.capabilities - List of capability names
|
|
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
|
|
34
|
+
* @param {string} [params.bodyData.beforeHandoff] - Before handoff checklist markdown
|
|
35
|
+
* @param {string[]} params.bodyData.constraints - List of constraints
|
|
36
|
+
* @returns {Object} Data object ready for Mustache template
|
|
37
|
+
*/
|
|
38
|
+
function prepareAgentProfileData({ frontmatter, bodyData }) {
|
|
39
|
+
// Trim array fields using helpers
|
|
40
|
+
const handoffs = trimFields(frontmatter.handoffs, { prompt: "required" });
|
|
41
|
+
const beforeMakingChanges = trimFields(bodyData.beforeMakingChanges, {
|
|
42
|
+
text: "required",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Trim simple string arrays
|
|
46
|
+
const constraints = (bodyData.constraints || []).map((c) => trimRequired(c));
|
|
47
|
+
const capabilities = (bodyData.capabilities || []).map((c) =>
|
|
48
|
+
trimRequired(c),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
// Frontmatter
|
|
53
|
+
name: frontmatter.name,
|
|
54
|
+
description: trimRequired(frontmatter.description),
|
|
55
|
+
infer: frontmatter.infer,
|
|
56
|
+
handoffs,
|
|
57
|
+
|
|
58
|
+
// Body data - trim all string fields
|
|
59
|
+
title: bodyData.title,
|
|
60
|
+
stageDescription: trimValue(bodyData.stageDescription),
|
|
61
|
+
identity: trimValue(bodyData.identity),
|
|
62
|
+
priority: trimValue(bodyData.priority),
|
|
63
|
+
capabilities,
|
|
64
|
+
beforeMakingChanges,
|
|
65
|
+
delegation: trimValue(bodyData.delegation),
|
|
66
|
+
operationalContext: trimValue(bodyData.operationalContext),
|
|
67
|
+
workingStyle: trimValue(bodyData.workingStyle),
|
|
68
|
+
beforeHandoff: trimValue(bodyData.beforeHandoff),
|
|
69
|
+
constraints,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
13
73
|
/**
|
|
14
74
|
* Format agent profile as .agent.md file content using Mustache template
|
|
15
75
|
* @param {Object} profile - Profile with frontmatter and bodyData
|
|
@@ -35,14 +95,6 @@ import Mustache from "mustache";
|
|
|
35
95
|
* @returns {string} Complete .agent.md file content
|
|
36
96
|
*/
|
|
37
97
|
export function formatAgentProfile({ frontmatter, bodyData }, template) {
|
|
38
|
-
const data = {
|
|
39
|
-
// Frontmatter
|
|
40
|
-
name: frontmatter.name,
|
|
41
|
-
description: frontmatter.description,
|
|
42
|
-
infer: frontmatter.infer,
|
|
43
|
-
handoffs: frontmatter.handoffs || [],
|
|
44
|
-
// Body data
|
|
45
|
-
...bodyData,
|
|
46
|
-
};
|
|
98
|
+
const data = prepareAgentProfileData({ frontmatter, bodyData });
|
|
47
99
|
return Mustache.render(template, data);
|
|
48
100
|
}
|