@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.
Files changed (51) hide show
  1. package/app/commands/agent.js +1 -1
  2. package/app/commands/index.js +4 -3
  3. package/app/commands/skill.js +56 -2
  4. package/app/commands/tool.js +112 -0
  5. package/app/components/checklist.js +6 -4
  6. package/app/components/markdown-textarea.js +132 -0
  7. package/app/css/components/forms.css +45 -0
  8. package/app/css/components/layout.css +12 -0
  9. package/app/css/components/surfaces.css +22 -0
  10. package/app/css/pages/detail.css +50 -0
  11. package/app/css/pages/job-builder.css +0 -42
  12. package/app/formatters/agent/profile.js +61 -9
  13. package/app/formatters/agent/skill.js +48 -6
  14. package/app/formatters/job/description.js +21 -16
  15. package/app/formatters/job/dom.js +9 -70
  16. package/app/formatters/shared.js +58 -0
  17. package/app/formatters/skill/dom.js +57 -2
  18. package/app/formatters/skill/markdown.js +18 -0
  19. package/app/formatters/skill/shared.js +12 -4
  20. package/app/formatters/stage/microdata.js +1 -1
  21. package/app/formatters/stage/shared.js +1 -1
  22. package/app/formatters/tool/shared.js +72 -0
  23. package/app/handout-main.js +7 -7
  24. package/app/index.html +10 -3
  25. package/app/lib/card-mappers.js +64 -17
  26. package/app/lib/render.js +4 -0
  27. package/app/lib/yaml-loader.js +12 -1
  28. package/app/main.js +4 -0
  29. package/app/model/agent.js +26 -18
  30. package/app/model/derivation.js +3 -3
  31. package/app/model/levels.js +2 -0
  32. package/app/model/loader.js +12 -1
  33. package/app/model/validation.js +74 -8
  34. package/app/pages/agent-builder.js +2 -2
  35. package/app/pages/landing.js +34 -14
  36. package/app/pages/self-assessment.js +7 -5
  37. package/app/pages/skill.js +5 -17
  38. package/app/pages/stage.js +10 -6
  39. package/app/pages/tool.js +50 -0
  40. package/app/slides/index.js +25 -25
  41. package/bin/pathway.js +31 -16
  42. package/examples/capabilities/business.yaml +17 -17
  43. package/examples/capabilities/delivery.yaml +51 -36
  44. package/examples/capabilities/reliability.yaml +127 -114
  45. package/examples/capabilities/scale.yaml +38 -36
  46. package/examples/disciplines/engineering_management.yaml +1 -1
  47. package/examples/framework.yaml +12 -0
  48. package/examples/grades.yaml +5 -7
  49. package/examples/self-assessments.yaml +1 -1
  50. package/package.json +1 -1
  51. package/templates/skill.template.md +31 -12
@@ -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) {
@@ -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";
@@ -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
- export const runSkillCommand = createEntityCommand({
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} capability - Capability ID
93
+ * @param {string} capabilityId - Capability ID
94
+ * @param {Array} capabilities - Capabilities array
94
95
  * @returns {string}
95
96
  */
96
- function formatCapabilityName(capability) {
97
- return capability.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
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);
@@ -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
  }