Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/agents/content/skills/common-mistakes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ Add no-automated-tests-in-test-plan rule to summarize-change and review-criteria

- Never include automated quality checks (CI, linting, type-checking, formatting) in test plans. They run automatically.

## Editing generated files

Files installed by the agents installer (under `~/.claude/`, `~/.agents/`, and other platform homes) are **generated artifacts**. The source of truth lives in `williamthorsen/codeassembly` under `packages/agents/content/`.

Look for a provenance marker at the top of the file. Generated files carry one of two formats:

- **YAML frontmatter:** three `# GENERATED FILE …` comment lines immediately after the opening `---`
- **No frontmatter:** three `<!-- GENERATED FILE … -->` comment lines at the top

If you see a marker, **do not edit the file in place** — the change will be silently overwritten on the next `codeassembly-agents install`. Instead:

1. Edit the source file in `williamthorsen/codeassembly` (the marker's `Source:` line links directly to it)
2. Open a PR against that repo
3. After merge, re-run `codeassembly-agents install` to pick up the change

## Cross-cutting issues

These mistakes span multiple categories:
Expand Down
141 changes: 141 additions & 0 deletions packages/agents/src/commands/__tests__/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,147 @@ describe('installCommand', () => {
expect(sourceContent).toContain('../_data/naming-conventions.md');
});

describe('provenance markers', () => {
const YAML_MARKER_LINE_1 = '# GENERATED FILE - Do not edit this file.';
const HTML_MARKER_LINE_1 = '<!-- GENERATED FILE - Do not edit this file. -->';

it('injects a YAML-comment marker into skill SKILL.md files', async () => {
const claudeHome = path.join(tempDir, '.claude');
await mkdir(path.join(claudeHome, 'skills'), { recursive: true });
await mkdir(path.join(claudeHome, 'agents'), { recursive: true });

await installCommand(makeOptions(), tempDir);

const skillPath = path.join(claudeHome, 'skills', 'commit', 'SKILL.md');
const content = await readFile(skillPath, 'utf8');
const lines = content.split('\n');

expect(lines[0]).toBe('---');
expect(lines[1]).toBe(YAML_MARKER_LINE_1);
expect(lines[2]).toMatch(
/^# Source: https:\/\/github\.com\/williamthorsen\/codeassembly\/blob\/main\/packages\/agents\/content\/skills\/commit\/SKILL\.md$/,
);
expect(lines[3]).toMatch(/^# Edits to this file are overwritten/);
});

it('injects markers into nested skill support files', async () => {
const claudeHome = path.join(tempDir, '.claude');
await mkdir(path.join(claudeHome, 'skills'), { recursive: true });
await mkdir(path.join(claudeHome, 'agents'), { recursive: true });

await installCommand(makeOptions(), tempDir);

// _data files have no frontmatter; marker should be HTML comment
const dataPath = path.join(claudeHome, 'skills', '_data', 'commit-format.md');
const content = await readFile(dataPath, 'utf8');

expect(content.startsWith(HTML_MARKER_LINE_1)).toBe(true);
expect(content).toContain(
'<!-- Source: https://github.com/williamthorsen/codeassembly/blob/main/packages/agents/content/skills/_data/commit-format.md -->',
);
});

it('injects a YAML-comment marker into subagent files', async () => {
const claudeHome = path.join(tempDir, '.claude');
await mkdir(path.join(claudeHome, 'skills'), { recursive: true });
await mkdir(path.join(claudeHome, 'agents'), { recursive: true });

await installCommand(makeOptions(), tempDir);

const subagentPath = path.join(claudeHome, 'agents', 'orchestrated-coder.md');
const content = await readFile(subagentPath, 'utf8');
const lines = content.split('\n');

expect(lines[0]).toBe('---');
expect(lines[1]).toBe(YAML_MARKER_LINE_1);
expect(lines[2]).toBe(
'# Source: https://github.com/williamthorsen/codeassembly/blob/main/packages/agents/content/subagents/orchestrated-coder.md',
);
// Subagent frontmatter keys (post-merge) still follow the marker
expect(content).toContain('permissionMode: bypassPermissions');
});

it('uses the platform-specific source URL for platform skills', async () => {
const claudeHome = path.join(tempDir, '.claude');
await mkdir(path.join(claudeHome, 'skills'), { recursive: true });
await mkdir(path.join(claudeHome, 'agents'), { recursive: true });

await installCommand(makeOptions({ platform: 'claude' }), tempDir);

// review-permissions is a claude-specific skill
const skillPath = path.join(claudeHome, 'skills', 'review-permissions', 'SKILL.md');
const content = await readFile(skillPath, 'utf8');
expect(content).toContain(
'# Source: https://github.com/williamthorsen/codeassembly/blob/main/packages/agents/content/skills/_platforms/claude/review-permissions/SKILL.md',
);
});

it('injects markers into shared guidance files in copy mode', async () => {
const claudeHome = path.join(tempDir, '.claude');
await mkdir(path.join(claudeHome, 'skills'), { recursive: true });
await mkdir(path.join(claudeHome, 'agents'), { recursive: true });

await installCommand(makeOptions({ link: false }), tempDir);

const sharedPath = path.join(tempDir, '.agents', 'AGENTS.md');
const content = await readFile(sharedPath, 'utf8');

// AGENTS.md has no frontmatter; expect HTML marker at top
expect(content.startsWith(HTML_MARKER_LINE_1)).toBe(true);
expect(content).toContain(
'<!-- Source: https://github.com/williamthorsen/codeassembly/blob/main/packages/agents/content/guidance/shared/AGENTS.md -->',
);
});

it('does NOT inject markers into shared guidance files installed as symlinks', async () => {
const claudeHome = path.join(tempDir, '.claude');
await mkdir(path.join(claudeHome, 'skills'), { recursive: true });
await mkdir(path.join(claudeHome, 'agents'), { recursive: true });

// Capture source content before install so we can verify it is unchanged afterward
const contentDir = resolveContentDir();
const sourcePath = path.join(contentDir, 'guidance', 'shared', 'AGENTS.md');
const sourceBefore = await readFile(sourcePath, 'utf8');

await installCommand(makeOptions({ link: true }), tempDir);

// The installed entry is a symlink
const sharedPath = path.join(tempDir, '.agents', 'AGENTS.md');
const stats = lstatSync(sharedPath);
expect(stats.isSymbolicLink()).toBe(true);

// The source file (the symlink's target) must NOT have been mutated: marker-free
// on input means marker-free on output. Marking the symlink target would corrupt
// the codeassembly source.
const sourceAfter = await readFile(sourcePath, 'utf8');
expect(sourceAfter).toBe(sourceBefore);
expect(sourceAfter.startsWith(HTML_MARKER_LINE_1)).toBe(false);
});

it('is idempotent: re-installing produces byte-identical marker output', async () => {
const claudeHome = path.join(tempDir, '.claude');
await mkdir(path.join(claudeHome, 'skills'), { recursive: true });
await mkdir(path.join(claudeHome, 'agents'), { recursive: true });

await installCommand(makeOptions(), tempDir);
const firstSkill = await readFile(path.join(claudeHome, 'skills', 'commit', 'SKILL.md'), 'utf8');
const firstData = await readFile(path.join(claudeHome, 'skills', '_data', 'commit-format.md'), 'utf8');
const firstSubagent = await readFile(path.join(claudeHome, 'agents', 'orchestrated-coder.md'), 'utf8');
const firstShared = await readFile(path.join(tempDir, '.agents', 'AGENTS.md'), 'utf8');

await installCommand(makeOptions(), tempDir);
const secondSkill = await readFile(path.join(claudeHome, 'skills', 'commit', 'SKILL.md'), 'utf8');
const secondData = await readFile(path.join(claudeHome, 'skills', '_data', 'commit-format.md'), 'utf8');
const secondSubagent = await readFile(path.join(claudeHome, 'agents', 'orchestrated-coder.md'), 'utf8');
const secondShared = await readFile(path.join(tempDir, '.agents', 'AGENTS.md'), 'utf8');

expect(secondSkill).toBe(firstSkill);
expect(secondData).toBe(firstData);
expect(secondSubagent).toBe(firstSubagent);
expect(secondShared).toBe(firstShared);
});
});

describe('installScripts', () => {
it('should place script files in the scripts directory after install', async () => {
const claudeHome = path.join(tempDir, '.claude');
Expand Down
27 changes: 24 additions & 3 deletions packages/agents/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import {
resolveSharedHome,
writeManifest,
} from '../lib/manifest.js';
import {
buildSourceUrl,
injectMarkerInFile,
injectMarkersInDirectory,
injectProvenanceMarker,
} from '../lib/marker-injector.js';
import { rewritePathsInDirectory, rewritePathsInFile } from '../lib/path-rewriter.js';
import { PLATFORMS, resolvePlatformIds, resolvePlatformPaths } from '../lib/platform.js';
import type {
Expand Down Expand Up @@ -171,6 +177,7 @@ async function installSkills(
path.join(skillsSrcDir, entry),
path.join(skillsDestDir, entry),
`skills/${entry}`,
`skills/${entry}`,
platformHome,
existingByPath,
options,
Expand Down Expand Up @@ -201,6 +208,7 @@ async function installSkills(
path.join(platformSkillsSrcDir, entry),
path.join(skillsDestDir, entry),
`skills/${entry}`,
`skills/_platforms/${platformId}/${entry}`,
platformHome,
existingByPath,
options,
Expand All @@ -224,6 +232,7 @@ async function installSkillEntry(
srcPath: string,
destPath: string,
relativePath: string,
sourceRelativeRoot: string,
platformHome: string,
existingByPath: ReadonlyMap<string, ManifestEntry>,
options: InstallOptions,
Expand All @@ -248,11 +257,14 @@ async function installSkillEntry(

await copyItem(srcPath, destPath);

// Rewrite Markdown paths and template variables for directories
// Rewrite Markdown paths, expand templates, and inject provenance markers for directories
const stats = await stat(srcPath);
if (stats.isDirectory()) {
const skillsDestDir = path.dirname(destPath);
await rewritePathsInDirectory(destPath, skillsDestDir, skillsPrefix, homeDir);
await injectMarkersInDirectory(destPath, (fileRelPath) => buildSourceUrl(`${sourceRelativeRoot}/${fileRelPath}`));
} else if (destPath.endsWith('.md')) {
await injectMarkerInFile(destPath, buildSourceUrl(sourceRelativeRoot));
}

return {
Expand Down Expand Up @@ -323,12 +335,13 @@ async function installSubagents(
}
}

// Read source, merge frontmatter, write to destination
// Read source, merge frontmatter, inject provenance marker, write to destination
const source = await readFile(srcPath, 'utf8');
const merged = mergeFrontmatter(source, overlayYaml);
const withMarker = injectProvenanceMarker(merged, buildSourceUrl(`subagents/${entry}`));
await mkdir(path.dirname(destPath), { recursive: true });
await unlinkIfSymlink(destPath);
await writeFile(destPath, merged, 'utf8');
await writeFile(destPath, withMarker, 'utf8');

const hash = await computeContentHash(destPath);
entries.push({
Expand Down Expand Up @@ -607,6 +620,13 @@ async function installSharedGuidance(
}

await (options.link ? linkItem(srcPath, destPath) : copyItem(srcPath, destPath));

// Copy-mode .md files receive a provenance marker. Link-mode entries are symlinks
// to the source file; marking them would mislabel the source itself.
if (!options.link && entry.endsWith('.md')) {
await injectMarkerInFile(destPath, buildSourceUrl(`guidance/shared/${entry}`));
}

anyWritten = true;

entries.push({
Expand Down Expand Up @@ -692,6 +712,7 @@ async function installPlatformGuidance(
// contains only absolute paths — no convention required for agents to resolve links.
if (entry.endsWith('.md')) {
await rewritePathsInFile(destPath, entry, platformConfig.homeDir, platformConfig.homeDir);
await injectMarkerInFile(destPath, buildSourceUrl(`guidance/_platforms/${platformId}/${entry}`));
}

entries.push({
Expand Down
138 changes: 138 additions & 0 deletions packages/agents/src/lib/__tests__/marker-injector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest';

import { buildSourceUrl, injectProvenanceMarker, SOURCE_REF } from '../marker-injector.js';

describe(injectProvenanceMarker, () => {
const sourceUrl =
'https://github.com/williamthorsen/codeassembly/blob/main/packages/agents/content/skills/example/SKILL.md';

describe('files with YAML frontmatter', () => {
it('inserts three comment lines immediately after the opening `---`', () => {
const input = ['---', 'name: example', 'description: an example skill', '---', '', '# Body', ''].join('\n');
const result = injectProvenanceMarker(input, sourceUrl);

expect(result).toBe(
[
'---',
'# GENERATED FILE - Do not edit this file.',
`# Source: ${sourceUrl}`,
'# Edits to this file are overwritten on the next install/sync. Edit the source and re-run `codeassembly-agents install`.',
'name: example',
'description: an example skill',
'---',
'',
'# Body',
'',
].join('\n'),
);
});

it('preserves the body exactly', () => {
const input = ['---', 'name: x', '---', '', 'Body line 1.', '', 'Body line 2.'].join('\n');
const result = injectProvenanceMarker(input, sourceUrl);
expect(result.endsWith('\n\nBody line 1.\n\nBody line 2.')).toBe(true);
});

it('is idempotent: applying twice returns the same content', () => {
const input = ['---', 'name: example', '---', '', 'Body', ''].join('\n');
const once = injectProvenanceMarker(input, sourceUrl);
const twice = injectProvenanceMarker(once, sourceUrl);
expect(twice).toBe(once);
});

it('refreshes the marker when the source URL differs', () => {
const input = ['---', 'name: example', '---', '', 'Body', ''].join('\n');
const withFirst = injectProvenanceMarker(input, sourceUrl);
const newUrl =
'https://github.com/williamthorsen/codeassembly/blob/main/packages/agents/content/skills/renamed/SKILL.md';
const withSecond = injectProvenanceMarker(withFirst, newUrl);

expect(withSecond).toContain(`# Source: ${newUrl}`);
expect(withSecond).not.toContain(`# Source: ${sourceUrl}`);
});
});

describe('files without YAML frontmatter', () => {
it('prepends three HTML comment lines and a trailing blank line', () => {
const input = '# AGENTS\n\nSome shared guidance.\n';
const result = injectProvenanceMarker(input, sourceUrl);

expect(result).toBe(
[
'<!-- GENERATED FILE - Do not edit this file. -->',
`<!-- Source: ${sourceUrl} -->`,
'<!-- Edits to this file are overwritten on the next install/sync. Edit the source and re-run `codeassembly-agents install`. -->',
'',
'# AGENTS',
'',
'Some shared guidance.',
'',
].join('\n'),
);
});

it('is idempotent: applying twice returns the same content', () => {
const input = '# AGENTS\n\nBody.\n';
const once = injectProvenanceMarker(input, sourceUrl);
const twice = injectProvenanceMarker(once, sourceUrl);
expect(twice).toBe(once);
});

it('refreshes the marker when the source URL differs', () => {
const input = '# AGENTS\n\nBody.\n';
const withFirst = injectProvenanceMarker(input, sourceUrl);
const newUrl =
'https://github.com/williamthorsen/codeassembly/blob/main/packages/agents/content/guidance/shared/OTHER.md';
const withSecond = injectProvenanceMarker(withFirst, newUrl);

expect(withSecond).toContain(`<!-- Source: ${newUrl} -->`);
expect(withSecond).not.toContain(`<!-- Source: ${sourceUrl} -->`);
});

it('handles empty input', () => {
const result = injectProvenanceMarker('', sourceUrl);
expect(result.startsWith('<!-- GENERATED FILE - Do not edit this file. -->\n')).toBe(true);
});
});

describe('edge cases', () => {
it('treats a `---` that is not at the start of the file as a non-frontmatter case', () => {
const input = 'Preface line\n---\nname: example\n---\n\nBody\n';
const result = injectProvenanceMarker(input, sourceUrl);
expect(result.startsWith('<!-- GENERATED FILE - Do not edit this file. -->\n')).toBe(true);
});

it('handles unicode content in the body', () => {
const input = ['---', 'name: example', '---', '', 'Emoji: 👍🏼', ''].join('\n');
const result = injectProvenanceMarker(input, sourceUrl);
expect(result).toContain('Emoji: 👍🏼');
});
});
});

describe(buildSourceUrl, () => {
it('builds a URL under packages/agents/content/ at the SOURCE_REF branch', () => {
expect(buildSourceUrl('skills/collaboration/SKILL.md')).toBe(
`https://github.com/williamthorsen/codeassembly/blob/${SOURCE_REF}/packages/agents/content/skills/collaboration/SKILL.md`,
);
});

it('handles nested paths', () => {
expect(buildSourceUrl('skills/orchestrate/modules/review-cycle.md')).toBe(
`https://github.com/williamthorsen/codeassembly/blob/${SOURCE_REF}/packages/agents/content/skills/orchestrate/modules/review-cycle.md`,
);
});

it('handles guidance and subagent paths', () => {
expect(buildSourceUrl('guidance/shared/AGENTS.md')).toBe(
`https://github.com/williamthorsen/codeassembly/blob/${SOURCE_REF}/packages/agents/content/guidance/shared/AGENTS.md`,
);
expect(buildSourceUrl('subagents/orchestrated-coder.md')).toBe(
`https://github.com/williamthorsen/codeassembly/blob/${SOURCE_REF}/packages/agents/content/subagents/orchestrated-coder.md`,
);
});

it('pins the source ref to main (until version-pinning is implemented)', () => {
expect(SOURCE_REF).toBe('main');
});
});
Loading
Loading