capabilities: read a plugin's where they live, stop copying them in
Four publishers copied a plugin's affordances, notifications and skills into capabilities/ at install. That directory is meant to be forked — core's work plus the owner's — so a fork accumulated files it did not author with nothing on disk saying which, and there was no good answer: commit them and carry another plugin's work, leave them and git status stays dirty. Copying was also wrong in two ways unrelated to git. A copy goes stale, because plugins:update pulls the plugin's directory and the copy keeps describing the old version. And a copy survived plugins:disable, so a disabled plugin's skill stayed in the agent's index — off, and still being announced. Now the loaders take a list of roots: the owner's store, plus each installed AND enabled plugin's own directory. capability-sources.ts resolves that list. No namespace is needed to keep them apart, because separate directories do it structurally rather than by convention. The owner's own win a skill name collision — their root is first and the merge keeps the first of each id — so a plugin cannot take over a name already in use. Affordances are deliberately NOT de-duplicated: several can be true at once. Notifications needed more than a root list, and it would have failed silently. Their codes were derived from the COPY'S PATH, <publisher>/<appName>/file.yaml. Read in place the path is just notifications/catalogue.yaml and carries no identity, so every plugin code would have vanished with nothing raised. loadPluginEntries takes the identity from the manifest instead, which is better provenance anyway — publisher and appName are validated at discovery, where a path was only ever a directory somebody chose. A yaml key still cannot claim another namespace, and that is pinned by test. createSession is async now, only because resolving enabled plugins reads the database. Once per session; its single caller already awaited. Deletes all four publishers including api/tasks/publish.ts from earlier today, superseded before it shipped. app-store/assets stays — images served over HTTP, where the copy is what makes them reachable. NOT done: tasks. That store is read-write, so reads must span sources while writes stay in the owner's directory, and a plugin's task has to be read-only in the UI. Next. Two things the design predicted as orphans turned out not to be, and both were checked before deleting rather than after. capabilities/extensions/tool-loader is written by the platform's own bootstrap every boot, not published by a plugin. And skills/email-db has no source to fall back on — the email plugin ships no skills directory, so that file exists in exactly one place and was hand-written. Neither was touched. 1422 pass, tsgo clean, officer restarts and serves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -552,11 +552,14 @@ function armIdle(session: PersistentSession): void {
|
||||
}, IDLE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function createSession(
|
||||
// Async only because the preamble is: resolving which plugins are installed and enabled, so their
|
||||
// capabilities can be read where they live, reads the database. Once per session — see
|
||||
// `capability-sources.ts`. Its single caller already awaits.
|
||||
async function createSession(
|
||||
params: ClaudeSpawnStreamingParams,
|
||||
onEvent: (event: ChatEvent) => void,
|
||||
onNotify: (kind: 'turn-ended' | 'input-needed') => void,
|
||||
): PersistentSession {
|
||||
): Promise<PersistentSession> {
|
||||
const { sessionKey } = params;
|
||||
const input = makeInputQueue();
|
||||
const abort = new AbortController();
|
||||
@@ -625,11 +628,11 @@ function createSession(
|
||||
|
||||
// Read here rather than at module load: a plugin installed an hour ago should be live on the next
|
||||
// message, and this costs a few kilobytes once per session.
|
||||
const affordances = affordanceBlock();
|
||||
const affordances = await affordanceBlock();
|
||||
// The skills INDEX — names, descriptions and paths, never bodies. See skills/load.ts for why this is
|
||||
// the inverse of an affordance. Read here rather than at module load for the same reason: a skill
|
||||
// written an hour ago should be offered on the next session.
|
||||
const skills = skillsBlock();
|
||||
const skills = await skillsBlock();
|
||||
/*
|
||||
And, for a commander, the two paths only this session can be told.
|
||||
|
||||
@@ -916,7 +919,7 @@ export async function spawnClaudeStreaming(
|
||||
await applyModelChange(session, params.model);
|
||||
await applyPlanModeChange(session, params.planMode);
|
||||
} else {
|
||||
session = createSession(params, onEvent, onNotify);
|
||||
session = await createSession(params, onEvent, onNotify);
|
||||
}
|
||||
session.pushTurn(params.prompt, params.images);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user