fix(ai): gate elements skill behind hasMcpTools flag (no prompt pollution)

Codex stop-hook #8: elements.md had `trigger: null` which makes
resolveSkills('generation', ...) unconditionally load its 1500-token
N-tool reference into every generation prompt — including the
embedded orchestrator in apps/web/src/services/ai that emits
single-shot JSON and CANNOT call MCP tools. The content was dead
weight there (orchestrator-sub-agent.ts:333 / ai-prompts.ts:132 both
build generation prompt via resolveSkills without any tool-use path).

Fix: trigger: { flags: [hasMcpTools] }. Skill only auto-loads when
caller explicitly declares MCP tools are available. No existing caller
sets this flag, so the embedded orchestrator prompt is now clean
again.

External MCP clients (Claude Code / Codex / Gemini CLI / Cursor) still
get the content via get_design_prompt(section='elements'), which uses
getSkillByName direct lookup and bypasses resolveSkills' trigger
filter. That contract is preserved.

Added explanatory HTML comment in the skill header so future editors
understand the gating + opt-in rule.

Tests: 4 new cases in design-prompt-elements.test.ts verify:
- getSkillByName returns skill regardless of flags (direct lookup)
- resolveSkills('generation') WITHOUT flag → elements excluded
- resolveSkills('generation') WITH {hasMcpTools:true} → elements included
- buildDesignPrompt('elements') works regardless of flag (bypass path)

14/14 design-prompt-elements tests pass; 186/186 across pen-mcp +
pen-ai-skills. format + tsc green. Bundle rebuilt.
This commit is contained in:
Fini 2026-04-19 19:06:35 +08:00
parent 43a6b7b86f
commit bff94b2ada
2 changed files with 52 additions and 1 deletions

View file

@ -2,12 +2,29 @@
name: elements
description: N-tool element family reference — when and how to call add_card_row_v0 / add_metric_row_v0 / add_nav_chip_row_v0 / add_bottom_nav_v0 / add_activity_ring_v0 instead of hand-building via batch_design
phase: [generation]
trigger: null
trigger:
flags: [hasMcpTools]
priority: 14
budget: 1500
category: base
---
<!--
IMPORTANT: This skill is gated by the `hasMcpTools` flag. It only
auto-loads into the generation-phase prompt when the caller declares
the AI has live access to MCP element tools (external clients:
Claude Code / Codex / Gemini CLI / Cursor). The embedded orchestrator
in apps/web emits single-shot JSON and cannot call MCP tools — this
skill would be 1500 tokens of dead weight there, so it stays excluded.
External MCP clients still retrieve the content explicitly via
get_design_prompt(section='elements'), which bypasses resolveSkills'
trigger filter (uses getSkillByName for direct lookup).
To opt-in auto-loading from a new caller: pass `{ flags: { hasMcpTools: true } }`
to resolveSkills('generation', prompt, opts).
-->
ELEMENT TOOLS (schema-constrained alternatives to batch_design):
These narrow MCP tools emit well-known structures that batch_design frequently gets wrong on non-Claude models (overflow, wrong role, anti-pattern layout). Each is shape-locked — you pick the tool by matching intent, then supply only content. Visual styling (color, font) stays orthogonal: override via a follow-up batch_design U-op if needed.

View file

@ -10,6 +10,7 @@
*/
import { describe, it, expect } from 'vitest';
import { getSkillByName, resolveSkills } from '@zseven-w/pen-ai-skills';
import { buildDesignPrompt, listPromptSections } from '../tools/design-prompt';
import { DESIGN_TOOL_DEFINITIONS } from '../routes/design-routes';
@ -82,3 +83,36 @@ describe('get_design_prompt — elements section', () => {
expect(unknown).toBe(full);
});
});
describe('elements skill — flag-gated auto-loading (no prompt pollution)', () => {
it('getSkillByName("elements") returns the skill regardless of flags (direct lookup)', () => {
const skill = getSkillByName('elements');
expect(skill).toBeDefined();
expect(skill?.meta.name).toBe('elements');
// Trigger is a flag gate, not unconditional loading
expect(skill?.meta.trigger).toEqual({ flags: ['hasMcpTools'] });
});
it('resolveSkills("generation") WITHOUT hasMcpTools flag → elements NOT auto-included', () => {
const ctx = resolveSkills('generation', 'create a dashboard', { flags: {} });
const names = ctx.skills.map((s) => s.meta.name);
expect(names).not.toContain('elements');
});
it('resolveSkills("generation") WITH hasMcpTools flag → elements IS auto-included', () => {
const ctx = resolveSkills('generation', 'create a dashboard', {
flags: { hasMcpTools: true },
});
const names = ctx.skills.map((s) => s.meta.name);
expect(names).toContain('elements');
});
it('buildDesignPrompt("elements") returns content regardless of flags (uses direct lookup, not resolver)', () => {
// get_design_prompt's section path bypasses resolveSkills — it uses
// getSkillByName directly. So the flag gate doesn't affect this path:
// external MCP clients asking for the section explicitly still get it.
const content = buildDesignPrompt('elements');
expect(content.length).toBeGreaterThan(500);
expect(content).toContain('add_card_row_v0');
});
});