Skip to content

Commit 1071d34

Browse files
fix(sync): parse skill descriptions with yaml (#67)
* test(sync): cover block scalar descriptions in skill index * fix(sync): parse skill descriptions with yaml ultramatter returns YAML block scalar indicators (`>`, `|-`) as part of the value, and the first-sentence split broke on any period, e.g. `.test.ts`. --------- Co-authored-by: paul valladares <85648028+dreyfus92@users.noreply.github.com>
1 parent f26e5e6 commit 1071d34

5 files changed

Lines changed: 90 additions & 32 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@bomb.sh/tools': patch
3+
---
4+
5+
Fixes `bsh sync` writing garbled or truncated skill descriptions to `AGENTS.md`

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@
7070
"publint": "^0.3.18",
7171
"tinyexec": "^1.1.1",
7272
"tsdown": "^0.22.3",
73-
"ultramatter": "^0.0.4",
7473
"vitest": "^4.1.2",
75-
"vitest-ansi-serializer": "^0.2.1"
74+
"vitest-ansi-serializer": "^0.2.1",
75+
"yaml": "^2.9.1"
7676
},
7777
"devDependencies": {
7878
"@changesets/cli": "^2.30.0",

‎pnpm-lock.yaml‎

Lines changed: 27 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/commands/sync.test.ts‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { lstat, readlink } from 'node:fs/promises';
22
import { fileURLToPath } from 'node:url';
33
import { describe, it, expect } from 'vitest';
44
import { createFixture, createMocks } from '../test-utils/index.ts';
5-
import { copySkills, findParentPackage } from './sync.ts';
5+
import { copySkills, findParentPackage, updateAgentsMd } from './sync.ts';
66

77
describe('copySkills', () => {
88
it('symlinks each skill into the destination', async () => {
@@ -53,6 +53,55 @@ describe('copySkills', () => {
5353
expect(await fixture.text('source-skills/build/SKILL.md')).toContain('name: build');
5454
expect(await fixture.text('project/skills/build/SKILL.md')).toContain('name: build');
5555
});
56+
57+
it('reads block scalar descriptions without the indicator', async () => {
58+
const fixture = await createFixture({
59+
'source-skills': {
60+
test: {
61+
'SKILL.md':
62+
'---\nname: test\ndescription: >\n Vitest test runner with colocated .test.ts files.\n Use when writing tests.\nmetadata:\n type: core\n---\nbody',
63+
},
64+
lint: {
65+
'SKILL.md': '---\nname: lint\ndescription: |-\n Lint the project.\n---\nbody',
66+
},
67+
},
68+
});
69+
70+
const skills = await copySkills({
71+
source: new URL('source-skills/', fixture.root),
72+
dest: new URL('project/skills/', fixture.root),
73+
});
74+
75+
expect(skills).toEqual(
76+
expect.arrayContaining([
77+
{
78+
name: 'test',
79+
description: 'Vitest test runner with colocated .test.ts files. Use when writing tests.',
80+
},
81+
{ name: 'lint', description: 'Lint the project.' },
82+
]),
83+
);
84+
});
85+
});
86+
87+
describe('updateAgentsMd', () => {
88+
it('summarizes each skill with its first full sentence', async () => {
89+
const fixture = await createFixture({ 'AGENTS.md': '# Project\n' });
90+
91+
await updateAgentsMd({
92+
root: fixture.root,
93+
skills: [
94+
{
95+
name: 'test',
96+
description: 'Vitest test runner with colocated .test.ts files. Use when writing tests.',
97+
},
98+
],
99+
});
100+
101+
expect(await fixture.text('AGENTS.md')).toContain(
102+
'- **test** — [skills/test/SKILL.md](skills/test/SKILL.md) - Vitest test runner with colocated .test.ts files\n',
103+
);
104+
});
56105
});
57106

58107
describe('findParentPackage', () => {

‎src/commands/sync.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { findPackageJSON } from 'node:module';
33
import { cwd, env, platform } from 'node:process';
44
import { fileURLToPath, pathToFileURL } from 'node:url';
55
import { NodeHfs } from '@humanfs/node';
6-
import { parse } from 'ultramatter';
6+
import { parse } from 'yaml';
77
import type { CommandContext } from '../context.ts';
88
import { relativeUrlPath, resolveLinkTarget } from '../utils.ts';
99

@@ -126,13 +126,13 @@ async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Pro
126126
await hfs.write(gitignorePath, content);
127127
}
128128

129-
async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> {
129+
export async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> {
130130
const { root, skills } = options;
131131
const agentsPath = new URL('AGENTS.md', root);
132132
let content = (await hfs.text(agentsPath)) ?? '';
133133

134134
const lines = skills.map((s) => {
135-
const desc = s.description.split('.')[0]?.trim();
135+
const desc = s.description.split(/\.(?:\s|$)/)[0]?.trim();
136136
return `- **${s.name}** — [skills/${s.name}/SKILL.md](skills/${s.name}/SKILL.md)${desc ? ` - ${desc}` : ''}`;
137137
});
138138

@@ -160,7 +160,9 @@ async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Prom
160160
}
161161

162162
function parseFrontmatter(content: string): SkillInfo | undefined {
163-
const { frontmatter } = parse(content);
163+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
164+
if (!match) return undefined;
165+
const frontmatter = parse(match[1]!) as Record<string, unknown> | null;
164166
if (!frontmatter) return undefined;
165167
const name = frontmatter.name as string | undefined;
166168
const description =

0 commit comments

Comments
 (0)