fix(daemon): inject @-mention skills into system prompt (#2552)

* fix(daemon): inject @-mention skills into system prompt

Generated-By: looper 0.8.1 (runner=worker, agent=opencode)

* fix(daemon): compose ad-hoc skill mode and aliases

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): lazily load and stage ad-hoc skills

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* test(daemon): assert staged skill files before spawn

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): compose skill metadata across @ mentions

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* test(daemon): cover ad-hoc critique skill policy

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): preserve plugin skill composition

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): resolve conflicting composed skill surfaces

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): preserve primary skill surface

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): share resolved critique surface

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)
This commit is contained in:
nettee 2026-05-21 22:20:21 +08:00 committed by GitHub
parent 682a9c9a9a
commit 052f8097de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 870 additions and 60 deletions

View file

@ -28,6 +28,7 @@
// source root so an environment that puts `skills/` itself behind a
// symlink (e.g. a content-addressable mount) is followed correctly.
import { createHash } from 'node:crypto';
import { cp, lstat, rm, stat } from 'node:fs/promises';
import path from 'node:path';
@ -44,6 +45,13 @@ export interface SkillStagingResult {
reason?: string;
}
export function skillCwdAliasSegment(dir: string): string {
const folder = path.basename(dir) || 'skill';
const normalizedDir = path.resolve(dir).replaceAll('\\', '/');
const digest = createHash('sha256').update(normalizedDir).digest('hex').slice(0, 10);
return `${folder}-${digest}`;
}
/**
* Copy `<sourceDir>` to `<cwd>/.od-skills/<folderName>/` so an agent can
* reach skill side files via a cwd-relative path. Idempotent and

View file

@ -5,6 +5,7 @@ import path from 'node:path';
import type { OrbitRunSummary, OrbitStatusResponse } from '@open-design/contracts/api/orbit';
import type { OrbitConfigPrefs } from './app-config.js';
import { skillCwdAliasSegment } from './cwd-aliases.js';
export interface OrbitConnectorRunResult {
connectorId: string;
@ -418,9 +419,9 @@ export function buildOrbitSystemPrompt(
'Selected example template:',
`- Skill id: ${template.id}`,
`- Skill name: ${template.name}`,
`- Staged root: .od-skills/${path.basename(template.dir)}/`,
`- Staged root: .od-skills/${skillCwdAliasSegment(template.dir)}/`,
'',
`Before writing the artifact, read ".od-skills/${path.basename(template.dir)}/SKILL.md" and, if present, ".od-skills/${path.basename(template.dir)}/example.html". Follow that staged template's structure, layout, tokens, domain rules, and visual language as the source of truth. The staged template is for visual/domain guidance; still use the live-artifact workflow to register the final artifact.`,
`Before writing the artifact, read ".od-skills/${skillCwdAliasSegment(template.dir)}/SKILL.md" and, if present, ".od-skills/${skillCwdAliasSegment(template.dir)}/example.html". Follow that staged template's structure, layout, tokens, domain rules, and visual language as the source of truth. The staged template is for visual/domain guidance; still use the live-artifact workflow to register the final artifact.`,
'',
'Selected template example prompt:',
'',

View file

@ -183,6 +183,37 @@ type AudioVoiceOption = {
labels?: Record<string, string> | null;
};
type ExclusiveSurfaceMode = 'deck' | 'image' | 'video' | 'audio';
const EXCLUSIVE_SURFACE_MODES = new Set<ExclusiveSurfaceMode>(['deck', 'image', 'video', 'audio']);
export function resolveExclusiveSurface(args: {
metadata?: ProjectMetadata | undefined;
skillMode?: ComposeInput['skillMode'] | undefined;
skillModes?: ComposeInput['skillModes'] | undefined;
}): ExclusiveSurfaceMode | null {
const activeSkillModes = new Set(
Array.isArray(args.skillModes)
? args.skillModes.filter(Boolean)
: args.skillMode
? [args.skillMode]
: [],
);
const metadataSurface = EXCLUSIVE_SURFACE_MODES.has(args.metadata?.kind as ExclusiveSurfaceMode)
? args.metadata?.kind as ExclusiveSurfaceMode
: null;
const primarySkillSurface = EXCLUSIVE_SURFACE_MODES.has(args.skillMode as ExclusiveSurfaceMode)
? args.skillMode as ExclusiveSurfaceMode
: null;
const composedSurfaceModes = Array.from(activeSkillModes).filter((mode): mode is ExclusiveSurfaceMode =>
EXCLUSIVE_SURFACE_MODES.has(mode as ExclusiveSurfaceMode),
);
return metadataSurface
?? primarySkillSurface
?? (composedSurfaceModes.length === 1 ? composedSurfaceModes[0] ?? null : null);
}
export const BASE_SYSTEM_PROMPT = OFFICIAL_DESIGNER_PROMPT;
export const SKIP_DISCOVERY_BRIEF_OVERRIDE = `# Automated project mode — skip discovery form
@ -235,6 +266,7 @@ export interface ComposeInput {
| 'video'
| 'audio'
| undefined;
skillModes?: Array<'prototype' | 'deck' | 'template' | 'design-system' | 'image' | 'video' | 'audio'> | undefined;
designSystemBody?: string | undefined;
designSystemTitle?: string | undefined;
// Compiled (machine-readable) form of the active brand's design system,
@ -346,6 +378,7 @@ export function composeSystemPrompt({
skillBody,
skillName,
skillMode,
skillModes,
designSystemBody,
designSystemTitle,
designSystemUsageMd,
@ -378,6 +411,14 @@ export function composeSystemPrompt({
// wording later in the official base prompt.
const parts: string[] = [];
const activeDesignSystemBody = designSystemBody?.trim();
const activeSkillModes = new Set(
Array.isArray(skillModes)
? skillModes.filter(Boolean)
: skillMode
? [skillMode]
: [],
);
const resolvedExclusiveSurface = resolveExclusiveSurface({ metadata, skillMode, skillModes });
// API/BYOK mode (streamFormat === 'plain'): mirrors the same fix from
// `@open-design/contracts`'s composer. The daemon hits this path for
@ -534,8 +575,8 @@ export function composeSystemPrompt({
// skeleton would conflict. The skill-seed path takes over via
// `derivePreflight` above, so we only fire the generic skeleton when no
// skill seed is on offer.
const isDeckProject = skillMode === 'deck' || metadata?.kind === 'deck';
const isFreeformProject = !skillMode && (!metadata || metadata.kind === 'other');
const isDeckProject = resolvedExclusiveSurface === 'deck';
const isFreeformProject = activeSkillModes.size === 0 && (!metadata || metadata.kind === 'other');
const hasSkillSeed =
!!skillBody && /assets\/template\.html/.test(skillBody);
if (isDeckProject && !hasSkillSeed) {
@ -556,12 +597,9 @@ export function composeSystemPrompt({
}
const isMediaSurface =
skillMode === 'image' ||
skillMode === 'video' ||
skillMode === 'audio' ||
metadata?.kind === 'image' ||
metadata?.kind === 'video' ||
metadata?.kind === 'audio';
resolvedExclusiveSurface === 'image'
|| resolvedExclusiveSurface === 'video'
|| resolvedExclusiveSurface === 'audio';
if (isMediaSurface) {
parts.push(MEDIA_GENERATION_CONTRACT);
}

View file

@ -18,6 +18,7 @@ import {
import {
composeSystemPrompt,
renderCodexImagegenOverride,
resolveExclusiveSurface,
shouldRenderCodexImagegenOverride,
} from './prompts/system.js';
import { expandHomePrefix, resolveProjectRelativePath } from './home-expansion.js';
@ -56,7 +57,12 @@ export {
signDesktopImportToken,
verifyDesktopImportToken,
} from './desktop-auth.js';
import { findSkillById, listSkills, splitDerivedSkillId } from './skills.js';
import {
findSkillById,
listSkills,
resolveSkillId,
splitDerivedSkillId,
} from './skills.js';
import { validateLinkedDirs } from './linked-dirs.js';
import { installFromTarget, uninstallById, sanitizeRepoName } from './library-install.js';
import { buildWindowsFolderDialogCommand, parseFolderDialogStdout } from './native-folder-dialog.js';
@ -215,7 +221,7 @@ import { listPromptTemplates, readPromptTemplate } from './prompt-templates.js';
import { buildDocumentPreview } from './document-preview.js';
import { lintArtifact, renderFindingsForAgent } from './lint-artifact.js';
import { loadCraftSections } from './craft.js';
import { stageActiveSkill } from './cwd-aliases.js';
import { skillCwdAliasSegment, stageActiveSkill } from './cwd-aliases.js';
import { buildDesktopPdfExportInput } from './pdf-export.js';
import { generateMedia } from './media.js';
import { listElevenLabsVoiceOptions } from './elevenlabs-voices.js';
@ -8765,6 +8771,7 @@ export async function startServer({
agentId,
projectId,
skillId,
skillIds,
designSystemId,
streamFormat,
locale,
@ -8782,35 +8789,131 @@ export async function startServer({
? designSystemId
: project?.designSystemId;
const metadata = project?.metadata;
let allSkillsPromise: ReturnType<typeof listAllSkillLikeEntries> | null = null;
const loadAllSkills = async () => {
allSkillsPromise ??= listAllSkillLikeEntries();
return await allSkillsPromise;
};
// Per-turn skills picked via the composer's @-mention popover. They
// never persist on the project — we just append their bodies after the
// primary skill so the agent sees one combined block this turn.
const effectiveCanonicalSkillId =
typeof effectiveSkillId === 'string' && effectiveSkillId
? resolveSkillId(effectiveSkillId)
: null;
const adHocSkillIds = Array.isArray(skillIds)
? skillIds
.map((s) => (typeof s === 'string' ? s.trim() : ''))
.filter(Boolean)
.filter((id) => resolveSkillId(id) !== effectiveCanonicalSkillId)
: [];
let skillBody;
let skillName;
let skillMode;
const skillModes = new Set<NonNullable<Parameters<typeof composeSystemPrompt>[0]['skillMode']>>();
let skillCraftRequires = [];
let activeSkillDir = null;
const activeSkillDirs: string[] = [];
// Per-skill Critique Theater override sourced from
// `od.critique.policy` in the resolved skill's SKILL.md frontmatter.
// `null` means the skill has no opinion and the lower-priority tiers
// (project override, env override, rollout phase default) decide.
let skillCritiquePolicy: SkillCritiquePolicy = null;
let critiqueSkillId = effectiveCanonicalSkillId;
const registerSkillMode = (
mode: NonNullable<Parameters<typeof composeSystemPrompt>[0]['skillMode']> | null | undefined,
) => {
if (!mode) return;
skillModes.add(mode);
};
const registerPrimarySkillMode = (
mode: NonNullable<Parameters<typeof composeSystemPrompt>[0]['skillMode']> | null | undefined,
) => {
if (!mode) return;
skillMode ??= mode;
registerSkillMode(mode);
};
const registerSkillDir = (dir: string | null | undefined) => {
if (typeof dir !== 'string' || dir.length === 0) return;
if (!activeSkillDir) activeSkillDir = dir;
if (!activeSkillDirs.includes(dir)) activeSkillDirs.push(dir);
};
const mergeSkillCritiquePolicy = (
current: SkillCritiquePolicy,
next: SkillCritiquePolicy,
): SkillCritiquePolicy => {
if (next === 'opt-out') return 'opt-out';
if (next === 'required') return current === 'opt-out' ? current : 'required';
if (next === 'opt-in') {
return current === 'required' || current === 'opt-out' ? current : 'opt-in';
}
return current;
};
if (effectiveSkillId) {
// Span both functional skills and design templates so a project
// saved against either surface keeps its system prompt after the
// skills/design-templates split. See specs/current/skills-and-design-templates.md.
const skill = findSkillById(
await listAllSkillLikeEntries(),
effectiveSkillId,
);
const allSkills = await loadAllSkills();
const skill = findSkillById(allSkills, effectiveSkillId);
if (skill) {
skillBody = skill.body;
skillName = skill.name;
skillMode = skill.mode;
activeSkillDir = skill.dir;
skillCritiquePolicy = skill.critiquePolicy;
registerPrimarySkillMode(skill.mode);
registerSkillDir(skill.dir);
skillCritiquePolicy = mergeSkillCritiquePolicy(
skillCritiquePolicy,
skill.critiquePolicy,
);
if (Array.isArray(skill.craftRequires))
skillCraftRequires = skill.craftRequires;
}
}
let composedSkillBlocks = '';
if (adHocSkillIds.length > 0) {
const allSkills = await loadAllSkills();
const seen = new Set(
effectiveCanonicalSkillId ? [String(effectiveCanonicalSkillId)] : [],
);
const blocks = [];
const baseBody = skillBody && skillBody.trim().length > 0 ? skillBody : '';
for (const id of adHocSkillIds) {
const canonicalId = resolveSkillId(id);
if (typeof canonicalId !== 'string' || canonicalId.length === 0) continue;
if (seen.has(canonicalId)) continue;
seen.add(canonicalId);
const extra = findSkillById(allSkills, id);
if (!extra) continue;
registerSkillDir(extra.dir);
registerSkillMode(extra.mode);
if (!effectiveCanonicalSkillId && adHocSkillIds.length === 1) {
registerPrimarySkillMode(extra.mode);
}
if (!critiqueSkillId || extra.critiquePolicy !== null) critiqueSkillId = canonicalId;
skillCritiquePolicy = mergeSkillCritiquePolicy(
skillCritiquePolicy,
extra.critiquePolicy,
);
if (Array.isArray(extra.craftRequires)) {
for (const craft of extra.craftRequires) {
if (!skillCraftRequires.includes(craft)) skillCraftRequires.push(craft);
}
}
blocks.push(
`\n\n---\n\n## Composed skill — ${extra.name || id}\n\n${(extra.body || '').trim()}`,
);
}
if (blocks.length > 0) {
composedSkillBlocks = blocks.join('');
skillBody = baseBody + composedSkillBlocks;
if (!skillName) {
skillName = adHocSkillIds.length === 1
? findSkillById(allSkills, adHocSkillIds[0])?.name ?? null
: 'composed';
}
}
}
// Stage A of plugin-driven-flow-plan: when the run is bound to a
// plugin snapshot, prefer the plugin's local SKILL.md (declared via
@ -8830,9 +8933,10 @@ export async function startServer({
const { loadPluginLocalSkill } = await import('./plugins/local-skill.js');
const local = await loadPluginLocalSkill(plugin);
if (local) {
skillBody = local.body;
skillBody = local.body + composedSkillBlocks;
skillName = local.name;
activeSkillDir = local.dir;
registerSkillDir(local.dir);
}
}
}
@ -9001,8 +9105,8 @@ export async function startServer({
&& typeof designSystemBody === 'string'
? { name: designSystemTitle, design_md: designSystemBody }
: undefined;
const critiqueSkill = critiqueEnabledForRun && typeof effectiveSkillId === 'string'
? { id: effectiveSkillId }
const critiqueSkill = critiqueEnabledForRun && typeof critiqueSkillId === 'string'
? { id: critiqueSkillId }
: undefined;
// Single-source-of-truth eligibility check. The composer downstream
// appends <CRITIQUE_RUN> instructions only when this check passes, and
@ -9016,13 +9120,15 @@ export async function startServer({
// panel addendum has to be suppressed here too: otherwise the model
// is instructed to emit Critique Theater tags that no orchestrator
// consumes.
const resolvedExclusiveSurface = resolveExclusiveSurface({
metadata,
skillMode,
skillModes: skillModes.size > 0 ? Array.from(skillModes) : undefined,
});
const isMediaSurface =
skillMode === 'image' ||
skillMode === 'video' ||
skillMode === 'audio' ||
metadata?.kind === 'image' ||
metadata?.kind === 'video' ||
metadata?.kind === 'audio';
resolvedExclusiveSurface === 'image'
|| resolvedExclusiveSurface === 'video'
|| resolvedExclusiveSurface === 'audio';
const isPlainAdapter = (streamFormat ?? 'plain') === 'plain';
const critiqueShouldRun = critiqueEnabledForRun
&& critiqueBrand !== undefined
@ -9089,6 +9195,7 @@ export async function startServer({
skillBody,
skillName,
skillMode,
skillModes: skillModes.size > 0 ? Array.from(skillModes) : undefined,
designSystemBody,
designSystemTitle,
designSystemUsageMd,
@ -9132,7 +9239,7 @@ export async function startServer({
// `listSkills()` scan in `startChatRun`. critiqueShouldRun threads
// the same panel-eligibility decision down to the spawn-path
// orchestrator gate so prompt and orchestrator stay in lockstep.
return { prompt, activeSkillDir, critiqueShouldRun };
return { prompt, activeSkillDir, activeSkillDirs, critiqueShouldRun };
};
// Plan §3.I1 / §3.D / spec §10.1: fire the pipeline schedule on a
@ -9222,6 +9329,7 @@ export async function startServer({
assistantMessageId,
clientRequestId,
skillId,
skillIds,
designSystemId,
attachments = [],
commentAttachments = [],
@ -9457,11 +9565,16 @@ export async function startServer({
.filter((s) => typeof oauthTokensForSpawn[s.id] === 'string')
.map((s) => ({ id: s.id, label: s.label }));
const { prompt: daemonSystemPrompt, activeSkillDir, critiqueShouldRun } =
const {
prompt: daemonSystemPrompt,
activeSkillDirs,
critiqueShouldRun,
} =
await composeDaemonSystemPrompt({
agentId,
projectId,
skillId,
skillIds,
designSystemId,
streamFormat: def?.streamFormat ?? 'plain',
locale,
@ -9477,11 +9590,11 @@ export async function startServer({
// advertises both the cwd-relative path (1) and the absolute path
// (2/3) so the agent can pick whichever works.
//
// 1. CWD-relative copy. Stage the *active* skill into
// 1. CWD-relative copy. Stage every active/composed skill into
// `<cwd>/.od-skills/<folder>/` so any agent CLI — not just the
// ones that honour `--add-dir` — can reach those files via a
// path inside its working directory. We copy (not symlink) so
// the staged directory is a true write barrier — agents cannot
// each staged directory is a true write barrier — agents cannot
// mutate the shipped repo resource through their cwd.
// 2. `--add-dir` allowlist. For non-Codex agents, pass `SKILLS_DIR`
// and `DESIGN_SYSTEMS_DIR` so the absolute fallback path in the
@ -9499,17 +9612,19 @@ export async function startServer({
// daemon and folded into the system prompt directly (see
// `readDesignSystem`), so an agent never has to open them via the
// filesystem.
if (cwd && activeSkillDir) {
const result = await stageActiveSkill(
cwd,
path.basename(activeSkillDir),
activeSkillDir,
(msg) => console.warn(msg),
);
if (!result.staged) {
console.warn(
`[od] skill-stage skipped: ${result.reason ?? 'unknown reason'}; falling back to absolute paths`,
if (cwd && activeSkillDirs.length > 0) {
for (const skillDir of activeSkillDirs) {
const result = await stageActiveSkill(
cwd,
skillCwdAliasSegment(skillDir),
skillDir,
(msg) => console.warn(msg),
);
if (!result.staged) {
console.warn(
`[od] skill-stage skipped: ${result.reason ?? 'unknown reason'}; falling back to absolute paths`,
);
}
}
}
// Resolve the agent's effective working directory once and use it
@ -10699,7 +10814,7 @@ export async function startServer({
const cwd = await ensureProject(PROJECTS_DIR, projectId);
const result = await stageActiveSkill(
cwd,
path.basename(template.dir),
skillCwdAliasSegment(template.dir),
template.dir,
(msg) => console.warn(msg),
);

View file

@ -11,7 +11,7 @@ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promi
import path from "node:path";
import { parseFrontmatter } from "./frontmatter.js";
import type { SkillCritiquePolicy } from "./critique/rollout.js";
import { SKILLS_CWD_ALIAS } from "./cwd-aliases.js";
import { skillCwdAliasSegment, SKILLS_CWD_ALIAS } from "./cwd-aliases.js";
// Persisted skill ids on existing projects can outlive a folder rename.
// listSkills() derives the id from the SKILL.md frontmatter `name`, so once
@ -396,7 +396,7 @@ export function splitDerivedSkillId(id: unknown): DerivedSkillIdParts | null {
// the right form on its own without daemon-side feature detection.
function withSkillRootPreamble(body: string, dir: string): string {
const referencedFiles = collectReferencedSideFiles(body);
const folder = path.basename(dir);
const folder = skillCwdAliasSegment(dir);
const skillRootRel = `${SKILLS_CWD_ALIAS}/${folder}`;
const exampleFile = referencedFiles[0];
const relativeGuidance = exampleFile

View file

@ -24,6 +24,7 @@ import {
startServer,
validateCodexGeneratedImagesDir,
} from '../src/server.js';
import { skillCwdAliasSegment } from '../src/cwd-aliases.js';
import { getAgentDef } from '../src/agents.js';
import { readMemoryConfig, writeMemoryConfig } from '../src/memory.js';
import { renderCodexImagegenOverride } from '../src/prompts/system.js';
@ -68,6 +69,39 @@ describe('/api/chat', () => {
const originalAgentHome = process.env.OD_AGENT_HOME;
const tempDirs: string[] = [];
async function createPluginFixture(args: {
pluginId: string;
dirName: string;
localSkillPath?: string;
}): Promise<string> {
const root = await fsp.mkdtemp(join(tmpdir(), 'od-plugin-fixture-'));
tempDirs.push(root);
const fixtureDir = resolve(root, args.dirName);
const baseFixtureDir = resolve(
process.cwd(),
'tests',
'fixtures',
'plugin-fixtures',
'sample-plugin',
);
await fsp.cp(baseFixtureDir, fixtureDir, { recursive: true });
const manifestPath = resolve(fixtureDir, 'open-design.json');
const manifest = JSON.parse(await fsp.readFile(manifestPath, 'utf8')) as {
name: string;
title: string;
od?: { context?: { skills?: Array<{ ref?: string; path?: string }> } };
};
manifest.name = args.pluginId;
manifest.title = args.pluginId;
if (args.localSkillPath) {
manifest.od ??= {};
manifest.od.context ??= {};
manifest.od.context.skills = [{ path: args.localSkillPath }];
}
await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
return fixtureDir;
}
beforeAll(async () => {
if (process.env.OD_DATA_DIR) {
originalMemoryConfig = await readMemoryConfig(process.env.OD_DATA_DIR);
@ -180,6 +214,582 @@ process.exit(0);
);
});
it('injects @-mention skillIds into the composed system prompt', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Composed skill — faq-page') ? 'has-composed-skill-header' : 'missing-composed-skill-header',
prompt.includes('# FAQ Page Skill') ? 'has-faq-skill-body' : 'missing-faq-skill-body',
prompt.includes('category filtering') ? 'has-faq-skill-content' : 'missing-faq-skill-content',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'build an faq page',
skillIds: ['faq-page'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-composed-skill-header');
expect(body).toContain('has-faq-skill-body');
expect(body).toContain('has-faq-skill-content');
expect(body).not.toContain('missing-composed-skill-header');
expect(body).not.toContain('missing-faq-skill-body');
expect(body).not.toContain('missing-faq-skill-content');
},
);
});
it('stages ad-hoc skill side files into the project cwd', async () => {
const projectId = `project-${randomUUID()}`;
const stagedRelativePath = `.od-skills/${skillCwdAliasSegment(resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager'))}/references/checklist.md`;
const expectedChecklist = await fsp.readFile(
resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager', 'references', 'checklist.md'),
'utf8',
);
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Ad hoc staged skill project',
}),
});
expect(createProjectResponse.ok).toBe(true);
const fakeAgentScript = `
const fs = require('node:fs');
const stagedChecklist = fs.readFileSync(${JSON.stringify(stagedRelativePath)}, 'utf8');
if (stagedChecklist !== ${JSON.stringify(expectedChecklist)}) {
console.error('staged-skill-side-files-mismatch');
process.exit(1);
}
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'staged-skill-side-files-before-spawn' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`;
await withFakeAgent(
'opencode',
fakeAgentScript,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'draft the release notes',
skillIds: ['release-notes-one-pager'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('staged-skill-side-files-before-spawn');
},
);
const stagedFileResponse = await fetch(
`${baseUrl}/api/projects/${projectId}/raw/${stagedRelativePath}`,
);
const stagedFileBody = await stagedFileResponse.text();
expect(stagedFileResponse.ok).toBe(true);
expect(stagedFileBody).toBe(expectedChecklist);
});
it('stages side files for every composed skill into the project cwd', async () => {
const projectId = `project-${randomUUID()}`;
const stagedPaths = [
`.od-skills/${skillCwdAliasSegment(resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager'))}/references/checklist.md`,
`.od-skills/${skillCwdAliasSegment(resolve(process.cwd(), '..', '..', 'skills', 'swiss-creative-mode-template'))}/references/checklist.md`,
] as const;
const expectedBodies = await Promise.all(
[
resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager', 'references', 'checklist.md'),
resolve(process.cwd(), '..', '..', 'skills', 'swiss-creative-mode-template', 'references', 'checklist.md'),
].map((file) => fsp.readFile(file, 'utf8')),
);
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Multi staged skill project',
}),
});
expect(createProjectResponse.ok).toBe(true);
const fakeAgentScript = `
const fs = require('node:fs');
const stagedBodies = [
fs.readFileSync(${JSON.stringify(stagedPaths[0])}, 'utf8'),
fs.readFileSync(${JSON.stringify(stagedPaths[1])}, 'utf8'),
];
const expectedBodies = ${JSON.stringify(expectedBodies)};
if (JSON.stringify(stagedBodies) !== JSON.stringify(expectedBodies)) {
console.error('multi-staged-skill-side-files-mismatch');
process.exit(1);
}
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'multi-staged-skill-side-files-before-spawn' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`;
await withFakeAgent(
'opencode',
fakeAgentScript,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'compose multiple skills',
skillIds: ['release-notes-one-pager', 'swiss-creative-mode-template'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('multi-staged-skill-side-files-before-spawn');
},
);
});
it('propagates the composed skill mode for ad-hoc-only deck skills', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Composed skill — open-design-landing-deck') ? 'has-deck-skill-header' : 'missing-deck-skill-header',
prompt.includes('# Slide deck — fixed framework (this is non-negotiable for deck mode)') ? 'has-deck-framework' : 'missing-deck-framework',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'build an editorial brand deck',
skillIds: ['open-design-landing-deck'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-deck-skill-header');
expect(body).toContain('has-deck-framework');
expect(body).not.toContain('missing-deck-skill-header');
expect(body).not.toContain('missing-deck-framework');
},
);
});
it('preserves a persisted media skill as the primary surface over a composed deck mention', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('# imagegen') ? 'has-base-image-skill-body' : 'missing-base-image-skill-body',
prompt.includes('## Composed skill — open-design-landing-deck') ? 'has-composed-deck-skill-header' : 'missing-composed-deck-skill-header',
prompt.includes('## Media generation contract (load-bearing — overrides softer wording above)') ? 'has-image-contract' : 'missing-image-contract',
prompt.includes('# Slide deck — fixed framework (this is non-negotiable for deck mode)') ? 'unexpected-deck-framework' : 'kept-deck-framework-out',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'generate an image while also referencing a deck template',
skillId: 'imagegen',
skillIds: ['open-design-landing-deck'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-base-image-skill-body');
expect(body).toContain('has-composed-deck-skill-header');
expect(body).toContain('has-image-contract');
expect(body).toContain('kept-deck-framework-out');
expect(body).not.toContain('missing-base-image-skill-body');
expect(body).not.toContain('missing-composed-deck-skill-header');
expect(body).not.toContain('missing-image-contract');
expect(body).not.toContain('unexpected-deck-framework');
},
);
});
it('propagates ad-hoc skill critique policy into the chat resolver', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for user skill critique-policy tests');
}
const skillId = `critique-opt-out-${randomUUID()}`;
const skillDir = resolve(process.env.OD_DATA_DIR, 'skills', skillId);
const originalCritiqueEnabled = process.env.OD_CRITIQUE_ENABLED;
await fsp.mkdir(skillDir, { recursive: true });
await fsp.writeFile(
resolve(skillDir, 'SKILL.md'),
`---
name: ${skillId}
description: Ad-hoc critique opt-out regression fixture.
od:
critique:
policy: opt-out
---
# Critique opt-out fixture
This skill should suppress critique when selected through skillIds.
`,
'utf8',
);
process.env.OD_CRITIQUE_ENABLED = 'true';
try {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Composed skill — ${skillId}') ? 'has-opt-out-skill-header' : 'missing-opt-out-skill-header',
prompt.includes('<CRITIQUE_RUN') ? 'unexpected-critique-panel' : 'critique-panel-disabled-by-skill-policy',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
designSystemId: 'default',
message: 'draft an opt-out skill artifact',
skillIds: [skillId],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-opt-out-skill-header');
expect(body).toContain('critique-panel-disabled-by-skill-policy');
expect(body).not.toContain('missing-opt-out-skill-header');
expect(body).not.toContain('unexpected-critique-panel');
},
);
} finally {
if (originalCritiqueEnabled == null) {
delete process.env.OD_CRITIQUE_ENABLED;
} else {
process.env.OD_CRITIQUE_ENABLED = originalCritiqueEnabled;
}
await fsp.rm(skillDir, { recursive: true, force: true });
}
});
it('preserves plugin-local and composed @-mention skills in plugin-bound runs', async () => {
const pluginId = `plugin-local-${randomUUID()}`;
const pluginFixtureDir = await createPluginFixture({
pluginId,
dirName: `plugin-local-${randomUUID()}`,
localSkillPath: './SKILL.md',
});
const installResponse = await fetch(`${baseUrl}/api/plugins/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', accept: 'text/event-stream' },
body: JSON.stringify({ source: pluginFixtureDir }),
});
const installBody = await installResponse.text();
expect(installResponse.status).toBe(200);
expect(installBody).toContain(`"id":"${pluginId}"`);
const projectId = `project-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin-bound skill composition project',
pluginId,
pluginInputs: { topic: 'agentic design' },
}),
});
const createProjectBody = await createProjectResponse.json() as {
appliedPluginSnapshotId?: string;
};
expect(createProjectResponse.ok).toBe(true);
expect(createProjectBody.appliedPluginSnapshotId).toBeTruthy();
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('# Sample Plugin') ? 'has-plugin-skill-body' : 'missing-plugin-skill-body',
prompt.includes('## Composed skill — faq-page') ? 'has-composed-skill-header' : 'missing-composed-skill-header',
prompt.includes('# FAQ Page Skill') ? 'has-composed-skill-body' : 'missing-composed-skill-body',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createRunResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'build a plugin-backed faq page',
appliedPluginSnapshotId: createProjectBody.appliedPluginSnapshotId,
skillIds: ['faq-page'],
}),
});
const createRunBody = await createRunResponse.json() as { runId: string };
expect(createRunResponse.status).toBe(202);
const eventsResponse = await fetch(`${baseUrl}/api/runs/${createRunBody.runId}/events`);
const body = await readSseUntil(eventsResponse, 'event: final');
expect(body).toContain('has-plugin-skill-body');
expect(body).toContain('has-composed-skill-header');
expect(body).toContain('has-composed-skill-body');
expect(body).not.toContain('missing-plugin-skill-body');
expect(body).not.toContain('missing-composed-skill-header');
expect(body).not.toContain('missing-composed-skill-body');
},
);
});
it('stages colliding plugin and composed skill dirs under distinct aliases', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for colliding skill-dir staging tests');
}
const pluginId = `plugin-collision-${randomUUID()}`;
const pluginFixtureDir = await createPluginFixture({
pluginId,
dirName: 'sample-plugin',
localSkillPath: './SKILL.md',
});
const installResponse = await fetch(`${baseUrl}/api/plugins/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', accept: 'text/event-stream' },
body: JSON.stringify({ source: pluginFixtureDir }),
});
const installBody = await installResponse.text();
expect(installResponse.status).toBe(200);
expect(installBody).toContain(`"id":"${pluginId}"`);
const projectId = `project-${randomUUID()}`;
const userSkillDir = resolve(process.env.OD_DATA_DIR, 'skills', 'sample-plugin');
const userChecklist = 'user-skill-checklist';
const userAlias = skillCwdAliasSegment(userSkillDir);
await fsp.mkdir(resolve(userSkillDir, 'references'), { recursive: true });
await fsp.writeFile(
resolve(userSkillDir, 'SKILL.md'),
'# Sample-plugin side-file fixture\n\nRead references/checklist.md before drafting.',
'utf8',
);
await fsp.writeFile(resolve(userSkillDir, 'references', 'checklist.md'), userChecklist, 'utf8');
try {
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Colliding skill-dir project',
pluginId,
pluginInputs: { topic: 'agentic design' },
}),
});
const createProjectBody = await createProjectResponse.json() as {
appliedPluginSnapshotId?: string;
};
const installedPluginResponse = await fetch(`${baseUrl}/api/plugins/${pluginId}`);
const installedPluginBody = await installedPluginResponse.json() as { fsPath: string };
const pluginAlias = skillCwdAliasSegment(installedPluginBody.fsPath);
expect(createProjectResponse.ok).toBe(true);
expect(installedPluginResponse.ok).toBe(true);
expect(createProjectBody.appliedPluginSnapshotId).toBeTruthy();
expect(pluginAlias).not.toBe(userAlias);
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
const pluginSkill = fs.readFileSync(${JSON.stringify(`.od-skills/${pluginAlias}/SKILL.md`)}, 'utf8');
const userChecklist = fs.readFileSync(${JSON.stringify(`.od-skills/${userAlias}/references/checklist.md`)}, 'utf8');
if (!pluginSkill.includes('# Sample Plugin')) {
console.error('plugin-skill-stage-missing');
process.exit(1);
}
if (userChecklist !== ${JSON.stringify(userChecklist)}) {
console.error('colliding-skill-stage-mismatch');
process.exit(1);
}
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'colliding-skill-dirs-staged' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createRunResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'use both plugin and user skill side files',
appliedPluginSnapshotId: createProjectBody.appliedPluginSnapshotId,
skillIds: ['sample-plugin'],
}),
});
const createRunBody = await createRunResponse.json() as { runId: string };
expect(createRunResponse.status).toBe(202);
const eventsResponse = await fetch(`${baseUrl}/api/runs/${createRunBody.runId}/events`);
const body = await readSseUntil(eventsResponse, 'event: final');
expect(body).toContain('colliding-skill-dirs-staged');
},
);
} finally {
await fsp.rm(userSkillDir, { recursive: true, force: true });
}
});
it('canonicalizes aliased skill ids before deduping composed skills', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const hasDuplicateComposedAlias = prompt.includes('## Composed skill — open-design-landing');
const checks = [
hasDuplicateComposedAlias ? 'duplicate-alias-composed-skill' : 'deduped-alias-composed-skill',
prompt.includes('# open-design-landing') ? 'has-base-alias-skill-body' : 'missing-base-alias-skill-body',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'build the Open Design landing page',
skillId: 'editorial-collage',
skillIds: ['open-design-landing'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('deduped-alias-composed-skill');
expect(body).toContain('has-base-alias-skill-body');
expect(body).not.toContain('duplicate-alias-composed-skill');
expect(body).not.toContain('missing-base-alias-skill-body');
},
);
});
it('classifies Cursor Agent authentication stderr as a typed run error', async () => {
await withFakeAgent(
'cursor-agent',

View file

@ -12,6 +12,7 @@ import {
type OrbitRunHandler,
type OrbitTemplateSelection,
} from '../src/orbit.js';
import { skillCwdAliasSegment } from '../src/cwd-aliases.js';
function formatExpectedLocalOrbitPromptTimestamp(date: Date): string {
const yyyy = date.getFullYear();
@ -80,11 +81,12 @@ describe('buildOrbitSystemPrompt', () => {
};
const prompt = buildOrbitSystemPrompt(new Date('2026-05-06T15:32:52.361Z'), template);
const stagedAlias = skillCwdAliasSegment(template.dir);
expect(prompt).toContain('Skill id: orbit-general');
expect(prompt).toContain('Staged root: .od-skills/orbit-general/');
expect(prompt).toContain('read ".od-skills/orbit-general/SKILL.md"');
expect(prompt).toContain('".od-skills/orbit-general/example.html"');
expect(prompt).toContain(`Staged root: .od-skills/${stagedAlias}/`);
expect(prompt).toContain(`read ".od-skills/${stagedAlias}/SKILL.md"`);
expect(prompt).toContain(`".od-skills/${stagedAlias}/example.html"`);
expect(prompt).toContain('visual/domain guidance');
expect(prompt).not.toContain('Selected template skill instructions:');
expect(prompt).toContain('Selected template example prompt:');

View file

@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { composeSystemPrompt } from '../../src/prompts/system.js';
import { composeSystemPrompt, resolveExclusiveSurface } from '../../src/prompts/system.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -246,6 +246,34 @@ describe('composeSystemPrompt', () => {
expect(prompt).not.toContain('**platformTargets**');
});
it('uses the primary skill surface when composed skill modes conflict', () => {
const prompt = composeSystemPrompt({
skillMode: 'image',
skillModes: ['deck', 'image'],
});
expect(prompt).toContain('## Media generation contract');
expect(prompt).not.toContain('# Slide deck — fixed framework');
});
it('lets metadata.kind win over conflicting composed skill modes', () => {
const prompt = composeSystemPrompt({
skillMode: 'image',
skillModes: ['deck', 'image'],
metadata: { kind: 'deck' } as any,
});
expect(prompt).toContain('# Slide deck — fixed framework');
expect(prompt).not.toContain('## Media generation contract');
});
it('resolves a non-media primary surface ahead of composed media mentions', () => {
expect(resolveExclusiveSurface({
skillMode: 'deck',
skillModes: ['deck', 'image'],
})).toBe('deck');
});
describe('artifact handoff no-emit clauses (#1143)', () => {
it('drops the absolute "non-negotiable" framing in favor of conditional language', () => {
const prompt = composeSystemPrompt({});

View file

@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest';
import { rmSync } from 'node:fs';
import { SKILLS_CWD_ALIAS } from '../src/cwd-aliases.js';
import { skillCwdAliasSegment, SKILLS_CWD_ALIAS } from '../src/cwd-aliases.js';
import { readFileSync } from 'node:fs';
import {
deleteUserSkill,
@ -87,14 +87,15 @@ describe('listSkills', () => {
previewType: 'html',
});
expect(skill.triggers.length).toBeGreaterThan(0);
const liveArtifactAlias = `${SKILLS_CWD_ALIAS}/${skillCwdAliasSegment(liveArtifactRoot)}`;
expect(skill.body).toContain(`> **Skill root (absolute fallback):** \`${liveArtifactRoot}\``);
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/live-artifact/`);
expect(skill.body).toContain(`${liveArtifactAlias}/`);
expect(skill.body).toContain('references/artifact-schema.md');
expect(skill.body).toContain('references/connector-policy.md');
expect(skill.body).toContain('references/refresh-contract.md');
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/live-artifact/references/artifact-schema.md`);
expect(skill.body).not.toContain(`${SKILLS_CWD_ALIAS}/live-artifact/assets/template.html`);
expect(skill.body).not.toContain(`${SKILLS_CWD_ALIAS}/live-artifact/references/layouts.md`);
expect(skill.body).toContain(`${liveArtifactAlias}/references/artifact-schema.md`);
expect(skill.body).not.toContain(`${liveArtifactAlias}/assets/template.html`);
expect(skill.body).not.toContain(`${liveArtifactAlias}/references/layouts.md`);
expect(skill.body).toContain('"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts create --input artifact.json');
expect(skill.body).toContain('do not ask “where should the data come from?” before checking daemon connector tools');
expect(skill.body).toContain('notion.notion_search');
@ -201,12 +202,14 @@ describe('listSkills preamble', () => {
const skill = skills[0];
if (!skill) throw new Error('demo-skill not found');
const demoAlias = `${SKILLS_CWD_ALIAS}/${skillCwdAliasSegment(path.join(root, 'demo-skill'))}`;
// The cwd-relative alias path is the primary one — that's what makes
// the agent stay inside its working directory when reading skill
// side files (issue #430).
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/demo-skill/`);
expect(skill.body).toContain(`${demoAlias}/`);
expect(skill.body).toContain(
`${SKILLS_CWD_ALIAS}/demo-skill/assets/template.html`,
`${demoAlias}/assets/template.html`,
);
// The absolute fallback is required for two cases the relative path
@ -233,8 +236,10 @@ describe('listSkills preamble', () => {
const skill = skills[0];
if (!skill) throw new Error('orbit-style skill not found');
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/orbit-style/`);
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/orbit-style/example.html`);
const orbitAlias = `${SKILLS_CWD_ALIAS}/${skillCwdAliasSegment(path.join(root, 'orbit-style'))}`;
expect(skill.body).toContain(`${orbitAlias}/`);
expect(skill.body).toContain(`${orbitAlias}/example.html`);
expect(skill.body).toContain('Known side files in this skill: `example.html`.');
});
@ -250,12 +255,15 @@ describe('listSkills preamble', () => {
const skill = skills[0];
if (!skill) throw new Error('magazine-web-ppt skill not found');
const folderAlias = `${SKILLS_CWD_ALIAS}/${skillCwdAliasSegment(path.join(root, 'guizang-ppt'))}`;
const frontmatterAlias = `${SKILLS_CWD_ALIAS}/${skillCwdAliasSegment(path.join(root, 'magazine-web-ppt'))}`;
// `id`/`name` reflect the frontmatter value (used elsewhere as a stable
// public id), but the on-disk alias path must use the actual folder
// name — that is what the daemon-staged junction maps to.
expect(skill.id).toBe('magazine-web-ppt');
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/guizang-ppt/`);
expect(skill.body).not.toContain(`${SKILLS_CWD_ALIAS}/magazine-web-ppt/`);
expect(skill.body).toContain(`${folderAlias}/`);
expect(skill.body).not.toContain(`${frontmatterAlias}/`);
});
it('does not emit a preamble for skills without side files', async () => {