Phase-1 telemetry-watcher's grafana-webhook handler 401'd because the
dashboard never mounted plugin-supplied routes — getPluginRoutes()
exists on PluginLoader but no caller consumed it. The smoke test was
working around this by injecting incident tasks directly through
/api/tasks; we want the real path to work end-to-end.
Two changes:
1. PluginLoader gains createContextFor(pluginId, { taskStore? }).
Lifecycle hooks still see the loader's bound store (the cwd
project), but REST handlers receive a project-scoped store derived
from the request's projectId so a Grafana webhook addressed to
sase opens tasks in sase even though fusion's loader is bound
to its own cwd. Settings still come from the loader's store at
load time, which is the right thing — settings don't follow the
request.
2. routes.ts iterates pluginLoader.getPluginRoutes() once at server
startup and binds /api/plugins/:pluginId/:routePath to a handler
that resolves project context per request, builds the context via
createContextFor, and forwards to the plugin's route. ApiError +
rethrowAsApiError preserve the dashboard's standard error envelope.
Plugins added after server start still need a restart for routes to
bind; reloadPlugin doesn't currently re-mount Express handlers. That
limitation matches the existing constraint and is out of scope here.