TODO Now: summarize pruned span + hot-reload installed entries

Summarize pruned span: droppedSpan + summarizeDiscarded (6k excerpt,
3-6 lines, one call per compaction) with retained note already landed;
flip TODO Now checkboxes to [x].

Hot-reload: Session.updateSkills/updatePlugins with pendingSkills/
pendingHost deferred during a turn, drainPendingHotReload at turn
boundary, live skill tool closure, cli.tsx rebuilds host from
builtin+registry+external on /registry add/remove. Guard now uses
live pluginHost. Test mid-session skill callability and plugin
enforcement without restart.

Spec: .hermes/plans/todo-now-next.md
This commit is contained in:
asepharyana
2026-09-08 23:54:37 +07:00
parent 4848db308a
commit c125cf36fa
5 changed files with 260 additions and 31 deletions
+27 -4
View File
@@ -429,9 +429,21 @@ const hooks: AppHooks = {
install: async (name) => {
const entry = await findEntry(name);
const { path } = await registry.install(entry);
// Loaded on the next start rather than hot-swapped: a skill joins the system
// prompt and a plugin joins the guard chain, and both are built once at boot.
return `installed ${entry.kind} ${entry.name} to ${path}\nrestart shiro to load it`;
// hot-reload: rebuild live catalogue so next turn sees it
if (entry.kind === 'skill') {
const fresh = await loadSkills();
session.updateSkills(fresh);
return `installed ${entry.kind} ${entry.name} to ${path}`;
}
if (entry.kind === 'plugin') {
const { plugins: freshPlugins, errors } = await registry.loadInstalledPlugins();
// re-compose from builtin + registry + external so order stays correct
const freshExternal = await loadExternalPlugins(process.cwd());
const host = createHost([...BUILTIN_PLUGINS.filter(pp => enabledPlugins.includes(pp.name)), ...freshPlugins, ...freshExternal.plugins], [...pluginErrors, ...errors, ...freshExternal.errors.map(e=>({plugin:e.name,message:e.message}))]);
session.updatePlugins(host);
return `installed ${entry.kind} ${entry.name} to ${path}`;
}
return `installed ${entry.kind} ${entry.name} to ${path}`;
},
remove: async (name) => {
const parsed = /^(skill|plugin):(.+)$/.exec(name);
@@ -439,7 +451,18 @@ const hooks: AppHooks = {
const bare = parsed ? parsed[2]! : name;
for (const kind of kinds) {
if (await registry.uninstall(kind, bare)) return `removed ${kind} ${bare}\nrestart shiro to unload it`;
if (await registry.uninstall(kind, bare)) {
if (kind === 'skill') {
const fresh = await loadSkills();
session.updateSkills(fresh);
} else {
const { plugins: freshPlugins, errors } = await registry.loadInstalledPlugins();
const freshExternal = await loadExternalPlugins(process.cwd());
const host = createHost([...BUILTIN_PLUGINS.filter(pp => enabledPlugins.includes(pp.name)), ...freshPlugins, ...freshExternal.plugins], [...pluginErrors, ...errors, ...freshExternal.errors.map(e=>({plugin:e.name,message:e.message}))]);
session.updatePlugins(host);
}
return `removed ${kind} ${bare}`;
}
}
throw new Error(`nothing installed under the name "${bare}"`);
},