feat(skill): layer the host skill registry per scope like the tools registry

The skills capability moves back to the host plane as one singleton, layered
with the ScopedLayers infrastructure the tools registry established: a
registration files into its calling context's layer (repository plugins and
host rows land global, a preset's skill-local lands in that preset's layer),
and a read merges the global layer with the viewing scope's chain, the
nearest layer winning duplicate names outright while rank keeps deciding
duplicates within one layer.

The web-app bundle re-enables the base skill registry row (skill-local and
tool-skill stay preset-owned), the standard preset drops its isolate skills
realm for bare rows over the host registry, tool-skill passes the calling
agent as the view scope, and the gateway's skills domain reads the host
registry in the presenter scope — a cold session now resolves its recorded
preset's standing key instead of failing.
This commit is contained in:
Yichen Jiang
2026-08-09 22:10:38 +08:00
parent 5aa10901ad
commit 4788569889
14 changed files with 495 additions and 114 deletions

View File

@@ -128,7 +128,9 @@ export function apply(ctx: Context, config: Config = {}): void {
if (!isSkillName(args.name)) {
throw new Error(`invalid skill name "${args.name}"`)
}
const lookup = { cwd: exec.agent?.session.header.cwd, signal: exec.signal }
// The agent is its own scope key, so the lookup resolves the layered
// registry exactly as this agent's composition sees it.
const lookup = { cwd: exec.agent?.session.header.cwd, signal: exec.signal, scope: exec.agent }
const summary = (await ctx.skills.list(lookup)).find(skill => skill.name === args.name)
if (!summary) {
throw new Error(`skill "${args.name}" is unknown or no longer available`)
@@ -181,7 +183,7 @@ export function apply(ctx: Context, config: Config = {}): void {
const names = invokedSkillNames(messages)
if (names.length === 0) return decision
signal.throwIfAborted()
const lookup = { cwd: agent.session.header.cwd, signal }
const lookup = { cwd: agent.session.header.cwd, signal, scope: agent }
const injections: UserMessage[] = []
for (const name of names) {
const skill = await ctx.skills.get(name, lookup)
@@ -217,7 +219,7 @@ export function apply(ctx: Context, config: Config = {}): void {
signal.throwIfAborted()
const toolVisible = ctx.tools.get(skillTool.name, agent) === skillTool
const snapshot = toolVisible
? await ctx.skills.snapshot({ cwd: agent.session.header.cwd, signal })
? await ctx.skills.snapshot({ cwd: agent.session.header.cwd, signal, scope: agent })
: { skills: [], complete: true }
signal.throwIfAborted()
if (!snapshot.complete) return decision