From 51de9de0eb2cb417ef8d2fba3bc54d0830a0c94d Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Sun, 19 Apr 2026 21:10:12 -0700 Subject: [PATCH 1/2] agents|feat: Add provenance markers to generated files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Injects a "GENERATED FILE - Do not edit this file." marker into every .md file written by the agents installer. Files with YAML frontmatter receive three `#` comment lines immediately after the opening `---`; files without frontmatter receive three HTML comment lines at the top. Each marker carries a `Source:` URL pointing to the canonical source in williamthorsen/codeassembly and instructions to edit the source and re-run `pnpm agents install` rather than editing the installed copy. Skips marker injection for shared-guidance entries installed as symlinks (`--link` mode), so the marker cannot pollute the symlink's target — which is the source file itself. Adds an "Editing generated files" section to the `common-mistakes` skill so agents that miss the in-file marker still encounter the rule via their catalog of mistakes to avoid. --- .../content/skills/common-mistakes/SKILL.md | 15 ++ .../src/commands/__tests__/install.test.ts | 141 +++++++++++++++++ packages/agents/src/commands/install.ts | 27 +++- .../src/lib/__tests__/marker-injector.test.ts | 138 +++++++++++++++++ packages/agents/src/lib/marker-injector.ts | 146 ++++++++++++++++++ 5 files changed, 464 insertions(+), 3 deletions(-) create mode 100644 packages/agents/src/lib/__tests__/marker-injector.test.ts create mode 100644 packages/agents/src/lib/marker-injector.ts diff --git a/packages/agents/content/skills/common-mistakes/SKILL.md b/packages/agents/content/skills/common-mistakes/SKILL.md index 6f880b5a..a8e0af60 100644 --- a/packages/agents/content/skills/common-mistakes/SKILL.md +++ b/packages/agents/content/skills/common-mistakes/SKILL.md @@ -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 `` 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 `pnpm 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 `pnpm agents install` to pick up the change + ## Cross-cutting issues These mistakes span multiple categories: diff --git a/packages/agents/src/commands/__tests__/install.test.ts b/packages/agents/src/commands/__tests__/install.test.ts index f49605c3..234798cd 100644 --- a/packages/agents/src/commands/__tests__/install.test.ts +++ b/packages/agents/src/commands/__tests__/install.test.ts @@ -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 = ''; + + 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( + '', + ); + }); + + 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( + '', + ); + }); + + 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'); diff --git a/packages/agents/src/commands/install.ts b/packages/agents/src/commands/install.ts index 9f678f45..2042bbf3 100644 --- a/packages/agents/src/commands/install.ts +++ b/packages/agents/src/commands/install.ts @@ -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 { @@ -171,6 +177,7 @@ async function installSkills( path.join(skillsSrcDir, entry), path.join(skillsDestDir, entry), `skills/${entry}`, + `skills/${entry}`, platformHome, existingByPath, options, @@ -201,6 +208,7 @@ async function installSkills( path.join(platformSkillsSrcDir, entry), path.join(skillsDestDir, entry), `skills/${entry}`, + `skills/_platforms/${platformId}/${entry}`, platformHome, existingByPath, options, @@ -224,6 +232,7 @@ async function installSkillEntry( srcPath: string, destPath: string, relativePath: string, + sourceRelativeRoot: string, platformHome: string, existingByPath: ReadonlyMap, options: InstallOptions, @@ -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 { @@ -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({ @@ -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({ @@ -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({ diff --git a/packages/agents/src/lib/__tests__/marker-injector.test.ts b/packages/agents/src/lib/__tests__/marker-injector.test.ts new file mode 100644 index 00000000..070a043e --- /dev/null +++ b/packages/agents/src/lib/__tests__/marker-injector.test.ts @@ -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 `pnpm 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( + [ + '', + ``, + '', + '', + '# 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(``); + expect(withSecond).not.toContain(``); + }); + + it('handles empty input', () => { + const result = injectProvenanceMarker('', sourceUrl); + expect(result.startsWith('\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('\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'); + }); +}); diff --git a/packages/agents/src/lib/marker-injector.ts b/packages/agents/src/lib/marker-injector.ts new file mode 100644 index 00000000..ecfffcf7 --- /dev/null +++ b/packages/agents/src/lib/marker-injector.ts @@ -0,0 +1,146 @@ +import { lstat, readdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +/** + * Git ref used in Source: URLs of provenance markers. Hardcoded until + * version-pinning lands (tracked in williamthorsen/codeassembly#444). + */ +export const SOURCE_REF = 'main'; + +const REPO_BLOB_BASE = `https://github.com/williamthorsen/codeassembly/blob/${SOURCE_REF}/packages/agents/content`; + +const LINE_1_TEXT = 'GENERATED FILE - Do not edit this file.'; +const LINE_3_TEXT = + 'Edits to this file are overwritten on the next install/sync. Edit the source and re-run `pnpm agents install`.'; + +const YAML_MARKER_PREFIX = '# '; +const HTML_MARKER_OPEN = ''; + +const FRONTMATTER_OPEN = '---'; + +/** + * Build the GitHub URL pointing to a source file under `packages/agents/content/`. + * `contentRelativePath` is the path relative to that directory (e.g., `skills/collaboration/SKILL.md`). + */ +export function buildSourceUrl(contentRelativePath: string): string { + return `${REPO_BLOB_BASE}/${contentRelativePath}`; +} + +/** + * Inject a provenance marker into a Markdown file's content. Files starting with `---\n` + * receive three YAML comment lines immediately after the opening delimiter; all other + * files receive three HTML comment lines at the top followed by a blank line. + * + * Idempotent: if the expected marker (any existing marker in the correct position) is already + * present, it is replaced with the marker computed from `sourceUrl`, leaving the surrounding + * content unchanged. A repeat call with the same `sourceUrl` returns identical content. + */ +export function injectProvenanceMarker(content: string, sourceUrl: string): string { + if (hasYamlFrontmatter(content)) { + return injectYamlMarker(content, sourceUrl); + } + return injectHtmlMarker(content, sourceUrl); +} + +/** + * Walk a directory tree and apply `injectProvenanceMarker` in place to every `.md` file found. + * `resolveSourceUrl` is called with each file's path relative to `rootDir` and must return the + * canonical source URL for that file. + */ +export async function injectMarkersInDirectory( + rootDir: string, + resolveSourceUrl: (fileRelPath: string) => string, +): Promise { + await walkAndInject(rootDir, rootDir, resolveSourceUrl); +} + +/** + * Apply `injectProvenanceMarker` in place to a single file. + */ +export async function injectMarkerInFile(filePath: string, sourceUrl: string): Promise { + const content = await readFile(filePath, 'utf8'); + const updated = injectProvenanceMarker(content, sourceUrl); + if (updated !== content) { + await writeFile(filePath, updated, 'utf8'); + } +} + +function hasYamlFrontmatter(content: string): boolean { + return content.startsWith(`${FRONTMATTER_OPEN}\n`); +} + +function injectYamlMarker(content: string, sourceUrl: string): string { + const lines = content.split('\n'); + // lines[0] is `---`. Strip any pre-existing marker lines immediately after it. + const afterOpen = stripExistingYamlMarkerLines(lines.slice(1)); + const markerLines = [ + `${YAML_MARKER_PREFIX}${LINE_1_TEXT}`, + `${YAML_MARKER_PREFIX}Source: ${sourceUrl}`, + `${YAML_MARKER_PREFIX}${LINE_3_TEXT}`, + ]; + return [FRONTMATTER_OPEN, ...markerLines, ...afterOpen].join('\n'); +} + +function stripExistingYamlMarkerLines(rest: ReadonlyArray): ReadonlyArray { + // An existing marker, if present, is three consecutive YAML comment lines whose first line + // begins with "# GENERATED FILE". Remove them so we can write fresh marker lines. This also + // handles the migration case of a different Source: URL. + if (rest.length >= 3 && rest[0] === `${YAML_MARKER_PREFIX}${LINE_1_TEXT}`) { + return rest.slice(3); + } + return rest; +} + +function injectHtmlMarker(content: string, sourceUrl: string): string { + const body = stripExistingHtmlMarkerBlock(content); + const marker = [ + `${HTML_MARKER_OPEN}${LINE_1_TEXT}${HTML_MARKER_CLOSE}`, + `${HTML_MARKER_OPEN}Source: ${sourceUrl}${HTML_MARKER_CLOSE}`, + `${HTML_MARKER_OPEN}${LINE_3_TEXT}${HTML_MARKER_CLOSE}`, + '', + ].join('\n'); + + if (body === '') { + return marker; + } + return `${marker}\n${body}`; +} + +function stripExistingHtmlMarkerBlock(content: string): string { + const existingFirstLine = `${HTML_MARKER_OPEN}${LINE_1_TEXT}${HTML_MARKER_CLOSE}`; + if (!content.startsWith(`${existingFirstLine}\n`)) { + return content; + } + const lines = content.split('\n'); + // Drop the three marker lines plus the single blank separator line, if present. + let dropCount = 3; + if (lines[dropCount] === '') { + dropCount++; + } + return lines.slice(dropCount).join('\n'); +} + +async function walkAndInject( + currentDir: string, + rootDir: string, + resolveSourceUrl: (fileRelPath: string) => string, +): Promise { + const entries = await readdir(currentDir); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry); + const stats = await lstat(fullPath); + + if (stats.isSymbolicLink()) { + continue; + } + + if (stats.isDirectory()) { + await walkAndInject(fullPath, rootDir, resolveSourceUrl); + } else if (entry.endsWith('.md')) { + const fileRelPath = path.relative(rootDir, fullPath).split(path.sep).join('/'); + await injectMarkerInFile(fullPath, resolveSourceUrl(fileRelPath)); + } + } +} From cf46d44917639134dbce0cc3dfe0ef60f08402aa Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Sun, 19 Apr 2026 21:55:27 -0700 Subject: [PATCH 2/2] agents|fix: Use correct CLI name in provenance marker text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces `pnpm agents install` with `codeassembly-agents install` in the marker's third line and in the `common-mistakes` guidance. The former is not a valid command — the CLI is installed as a bin entry named `codeassembly-agents`, not as a pnpm workspace script. --- packages/agents/content/skills/common-mistakes/SKILL.md | 4 ++-- packages/agents/src/lib/__tests__/marker-injector.test.ts | 4 ++-- packages/agents/src/lib/marker-injector.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/agents/content/skills/common-mistakes/SKILL.md b/packages/agents/content/skills/common-mistakes/SKILL.md index a8e0af60..f4d894eb 100644 --- a/packages/agents/content/skills/common-mistakes/SKILL.md +++ b/packages/agents/content/skills/common-mistakes/SKILL.md @@ -74,11 +74,11 @@ Look for a provenance marker at the top of the file. Generated files carry one o - **YAML frontmatter:** three `# GENERATED FILE …` comment lines immediately after the opening `---` - **No frontmatter:** three `` 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 `pnpm agents install`. Instead: +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 `pnpm agents install` to pick up the change +3. After merge, re-run `codeassembly-agents install` to pick up the change ## Cross-cutting issues diff --git a/packages/agents/src/lib/__tests__/marker-injector.test.ts b/packages/agents/src/lib/__tests__/marker-injector.test.ts index 070a043e..35021574 100644 --- a/packages/agents/src/lib/__tests__/marker-injector.test.ts +++ b/packages/agents/src/lib/__tests__/marker-injector.test.ts @@ -16,7 +16,7 @@ describe(injectProvenanceMarker, () => { '---', '# 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 `pnpm agents install`.', + '# 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', '---', @@ -61,7 +61,7 @@ describe(injectProvenanceMarker, () => { [ '', ``, - '', + '', '', '# AGENTS', '', diff --git a/packages/agents/src/lib/marker-injector.ts b/packages/agents/src/lib/marker-injector.ts index ecfffcf7..c189f5b6 100644 --- a/packages/agents/src/lib/marker-injector.ts +++ b/packages/agents/src/lib/marker-injector.ts @@ -11,7 +11,7 @@ const REPO_BLOB_BASE = `https://github.com/williamthorsen/codeassembly/blob/${SO const LINE_1_TEXT = 'GENERATED FILE - Do not edit this file.'; const LINE_3_TEXT = - 'Edits to this file are overwritten on the next install/sync. Edit the source and re-run `pnpm agents install`.'; + 'Edits to this file are overwritten on the next install/sync. Edit the source and re-run `codeassembly-agents install`.'; const YAML_MARKER_PREFIX = '# '; const HTML_MARKER_OPEN = '