diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 5f000f7ed..43b41d915 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -35,8 +35,8 @@ "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets && npm run bundle", "build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets", - "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx rm -rf dist/prime-agent-runtime && shx cp -r ../../prime-agent-runtime dist/prime-agent-runtime", - "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp -r skills dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/", + "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx rm -rf dist/prime-agent-runtime && shx cp -r ../../prime-agent-runtime dist/prime-agent-runtime && shx rm -rf dist/skills && shx cp -r skills dist/skills", + "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/", "test": "vitest --run", "postinstall": "node postinstall.cjs", "prepublishOnly": "npm run clean && npm run build", diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index f7ed1d843..db4e9d205 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -31,6 +31,7 @@ import { CONFIG_DIR_NAME, getBundledSkillsDir } from "../config.js"; import { shouldUseWindowsShell } from "../utils/child-process.js"; import { type GitSource, parseGitUrl } from "../utils/git.js"; import { canonicalizePath, isLocalPath } from "../utils/paths.js"; +import type { ResourceDiagnostic } from "./diagnostics.js"; import { isStdoutTakenOver } from "./output-guard.js"; import type { PackageSource, SettingsManager } from "./settings-manager.js"; @@ -62,6 +63,7 @@ export interface ResolvedPaths { skills: ResolvedResource[]; prompts: ResolvedResource[]; themes: ResolvedResource[]; + diagnostics: ResourceDiagnostic[]; } export type MissingSourceAction = "install" | "skip" | "error"; @@ -158,6 +160,7 @@ interface ResourceAccumulator { skills: Map; prompts: Map; themes: Map; + diagnostics: ResourceDiagnostic[]; } /** @@ -2242,13 +2245,20 @@ export class DefaultPackageManager implements PackageManager { origin: "top-level", baseDir: this.bundledSkillsDir, }; - addResources( - "skills", - collectAutoSkillEntries(this.bundledSkillsDir, "pi"), - builtinMetadata, - userOverrides.skills, - this.bundledSkillsDir, - ); + const builtinEntries = collectAutoSkillEntries(this.bundledSkillsDir, "pi"); + // Built-in skills (edit, goal, …) are expected to ship with the package. A + // packaging slip that drops the skills/ dir from the build output would + // otherwise degrade silently to zero skills (ENG-4220); surface it loudly. + if (builtinEntries.length === 0) { + accumulator.diagnostics.push({ + type: "warning", + message: existsSync(this.bundledSkillsDir) + ? "built-in skills directory contains no skills; this build may be packaged incorrectly" + : "built-in skills directory not found; this build may be packaged incorrectly", + path: this.bundledSkillsDir, + }); + } + addResources("skills", builtinEntries, builtinMetadata, userOverrides.skills, this.bundledSkillsDir); } addResources( "prompts", @@ -2321,6 +2331,7 @@ export class DefaultPackageManager implements PackageManager { skills: new Map(), prompts: new Map(), themes: new Map(), + diagnostics: [], }; } @@ -2349,6 +2360,7 @@ export class DefaultPackageManager implements PackageManager { skills: mapToResolved(accumulator.skills), prompts: mapToResolved(accumulator.prompts), themes: mapToResolved(accumulator.themes), + diagnostics: accumulator.diagnostics, }; } diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 8ac486737..7ccc896e4 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -425,6 +425,8 @@ export class DefaultResourceLoader implements ResourceLoader { this.lastSkillPaths = skillPaths; this.updateSkillsFromPaths(skillPaths, metadataByPath); + // Surface resolution-time skill warnings (e.g. missing bundled skills dir). + this.skillDiagnostics.push(...resolvedPaths.diagnostics); for (const p of this.additionalSkillPaths) { if (isLocalPath(p) && !existsSync(p) && !this.skillDiagnostics.some((d) => d.path === p)) { this.skillDiagnostics.push({ type: "error", message: "Skill path does not exist", path: p }); diff --git a/packages/coding-agent/test/builtin-skills.test.ts b/packages/coding-agent/test/builtin-skills.test.ts index e3df4c2aa..768ef0982 100644 --- a/packages/coding-agent/test/builtin-skills.test.ts +++ b/packages/coding-agent/test/builtin-skills.test.ts @@ -98,6 +98,61 @@ describe("builtin skills", () => { expect(result.skills.some((r) => r.metadata.source === "builtin")).toBe(false); }); + it("warns when the bundled skills directory is missing (packaging slip)", async () => { + const missingDir = join(tempDir, "does-not-exist"); + const packageManager = new DefaultPackageManager({ + cwd, + agentDir, + settingsManager, + bundledSkillsDir: missingDir, + }); + const result = await packageManager.resolve(); + + const warning = result.diagnostics.find((d) => d.path === missingDir && d.type === "warning"); + expect(warning).toBeDefined(); + expect(warning?.message).toContain("built-in skills"); + }); + + it("warns when the bundled skills directory exists but is empty", async () => { + const packageManager = new DefaultPackageManager({ + cwd, + agentDir, + settingsManager, + bundledSkillsDir: bundledDir, + }); + const result = await packageManager.resolve(); + + const warning = result.diagnostics.find((d) => d.path === bundledDir && d.type === "warning"); + expect(warning).toBeDefined(); + }); + + it("does not warn when bundled skills are present", async () => { + writeSkill(bundledDir, "builtin-skill"); + const packageManager = new DefaultPackageManager({ + cwd, + agentDir, + settingsManager, + bundledSkillsDir: bundledDir, + }); + const result = await packageManager.resolve(); + + expect(result.diagnostics.some((d) => d.path === bundledDir)).toBe(false); + }); + + it("does not warn when built-in skills are disabled", async () => { + const missingDir = join(tempDir, "does-not-exist"); + settingsManager.setEnableBuiltinSkills(false); + const packageManager = new DefaultPackageManager({ + cwd, + agentDir, + settingsManager, + bundledSkillsDir: missingDir, + }); + const result = await packageManager.resolve(); + + expect(result.diagnostics.some((d) => d.path === missingDir)).toBe(false); + }); + it("disables individual bundled skills via settings override patterns", async () => { writeSkill(bundledDir, "builtin-skill"); writeSkill(bundledDir, "other-skill"); @@ -151,6 +206,15 @@ describe("builtin skills", () => { expect(loader.getSkills().skills).toEqual([]); }); + + it("surfaces a skill diagnostic when the bundled skills dir is missing", async () => { + const missingDir = join(tempDir, "does-not-exist"); + const loader = new DefaultResourceLoader({ cwd, agentDir, bundledSkillsDir: missingDir }); + await loader.reload(); + + const { diagnostics } = loader.getSkills(); + expect(diagnostics.some((d) => d.path === missingDir && d.type === "warning")).toBe(true); + }); }); describe("shipped skill content", () => { @@ -214,4 +278,33 @@ describe("builtin skills", () => { expect(skills[0].kind === "python" && skills[0].python.importName).toBe("word_count"); }); }); + + // The bundled skills only reach a deployed agent if the build/release scripts + // copy skills/ into the shipped layout. A regression here resolves zero + // built-in skills at runtime even though the source tree looks correct + // (ENG-4220), so assert each shipping path still copies skills. + describe("packaging ships bundled skills", () => { + const packageRoot = join(__dirname, ".."); + const repoRoot = join(packageRoot, "..", ".."); + + it("npm build (copy-assets) copies skills into dist", () => { + const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf-8")) as { + files?: string[]; + scripts?: Record; + }; + expect(pkg.scripts?.["copy-assets"]).toContain("skills dist/skills"); + // npm publish ships the source skills/ dir via the files allowlist too. + expect(pkg.files).toContain("skills"); + }); + + it("binary release script copies skills next to the executable", () => { + const script = readFileSync(join(repoRoot, "scripts", "build-binaries.sh"), "utf-8"); + expect(script).toMatch(/cp -r skills binaries\/\$platform\//); + }); + + it("release packer includes skills in the packed package", () => { + const script = readFileSync(join(repoRoot, "scripts", "pack-prime-agent-release.mjs"), "utf-8"); + expect(script).toContain('"skills"'); + }); + }); }); diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index 635aca64f..cbc029f81 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -128,6 +128,7 @@ for platform in "${PLATFORMS[@]}"; do cp -r dist/core/export-html binaries/$platform/ cp -r docs binaries/$platform/ cp -r examples binaries/$platform/ + cp -r skills binaries/$platform/ # Copy koffi native module for Windows (needed for VT input support) if [[ "$platform" == "windows-x64" ]]; then diff --git a/scripts/pack-prime-agent-release.mjs b/scripts/pack-prime-agent-release.mjs index a3fa84d18..4d419fec0 100644 --- a/scripts/pack-prime-agent-release.mjs +++ b/scripts/pack-prime-agent-release.mjs @@ -196,7 +196,7 @@ function copyPackageContents(sourceDir, targetDir, packageJson) { mkdirSync(targetDir, { recursive: true }); writeJson(join(targetDir, "package.json"), packageJson); - for (const entry of ["dist", "docs", "examples", "postinstall.cjs", "README.md", "CHANGELOG.md"]) { + for (const entry of ["dist", "docs", "examples", "skills", "postinstall.cjs", "README.md", "CHANGELOG.md"]) { copyIfExists(join(sourceDir, entry), join(targetDir, entry)); } }