Fusion's plugin loader imports source files directly via Node 22 ESM
without a TS loader, so relative imports like "./internal/dedup.js"
fail at runtime: there's no compiled .js artifact and Node won't fall
back to .ts. Inline severity classifier, dedup cache, rate limiter,
and Grafana parser into the single entry file. Tests now import named
exports from "../index.js" directly. The Phase-2 split into separate
source files can come back once fusion adds a TS-aware plugin loader.
All 14 unit tests still pass. Behavior unchanged.
External services (Grafana, Sentry, Slack) call plugin webhooks with
their own per-plugin shared secret — they cannot present the
dashboard's daemon token. Daemon auth was 401'ing those callbacks
before they reached the plugin handler, so even with the route
correctly mounted the secret check inside the plugin never fired.
Add a registry of dynamically-exempt paths populated at server
startup when plugin routes are mounted. Plugin management routes
(/api/plugins, /api/plugins/:id/enable, etc.) stay gated; only the
plugin-defined routes (/api/plugins/:pluginId/<route>) are exempted.
Each plugin handler is responsible for its own secret check (the
telemetry-watcher webhook compares Authorization Bearer against
settings.grafanaWebhookSecret in constant time at the handler).
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.
New workspace package fusion-plugin-telemetry-watcher that turns a
Grafana Alerting webhook payload into a Fusion incident task in the
triage column. Hooks the dedup/severity/rate-limit primitives that
PostHog/Sentry/Slack sources will reuse in Phase 2.
Pipeline:
POST /api/plugins/fusion-plugin-telemetry-watcher/grafana-webhook
→ bearer-secret check
→ parseGrafanaPayload (one signal per firing alert; resolved alerts
are dropped — recovery is verified post-deploy by the QA agent)
→ classifySeverity P0/P1/P2/P3 with critical-path keyword
escalation (payment/auth/billing/subscription)
→ DedupCache 4h fingerprint window — repeat fires log against the
existing task instead of opening duplicates
→ IncidentRateLimiter 5/h, 20/d — overflow becomes a "telemetry
storm" mega-task in a future phase
→ taskStore.createTask({ column: "triage", priority })
→ optional auto-assign to Triage Agent
14 unit tests cover severity buckets, critical-path escalation, dedup
windowing/eviction, hourly+daily rate caps, and the Grafana payload
parser (firing vs resolved, label-based domain inference).
Settings expose all thresholds + secret + dedup window + rate limits
through the dashboard plugin settings UI. README documents the deploy
+ register + Grafana contact-point wiring.
ProjectEngineManager runs one engine per registered project, but the
heartbeat handler in register-agent-runtime-routes.ts only used the
single global heartbeatMonitor passed via ServerOptions. That monitor
is bound to the cwd project's engine, so heartbeat triggers for any
secondary project silently no-op'd: the run id was returned but no
execution actually happened.
Add resolveHeartbeatMonitorFor(scopedStore) that walks
engineManager.getAllEngines() and returns the engine whose working
directory matches the request's scoped store, falling back to the
global monitor when its rootDir matches. Use the resolver at every
heartbeat call site (state-pause stop, /agents/:id/heartbeat,
/agents/:id/runs, /agents/:id/runs/stop).
The host's /usr/bin/codex is a symlink to the codex.js entrypoint inside
the package directory; bind-mounting it directly resolves to the file
content but breaks codex's import.meta.url-based module resolution. Add
the symlink at build time so a /usr/lib/node_modules/@openai/codex bind
mount is enough to make codex usable in the container.
The dashboard modules used a variable-specifier dynamic import
(`const m = "@fusion/engine"; await import(m)`) to defeat bundler static
analysis. tsup honored that and left the dynamic import in dist/bin.js,
so the published `@runfusion/fusion` package failed at runtime with
"createFnAgent2 is not a function" — `@fusion/engine` isn't on npm and
the silent catch set the binding to undefined. Replaces the trick with
static imports across planning, chat, subtask-breakdown, mission-interview,
agent-generation, ai-refine, roadmap-suggestions, milestone-slice-interview,
and routes. Core can't statically import engine (cycle), so it now exposes
setCreateFnAgent and engine wires itself in at module load. Documents the
pattern in AGENTS.md.
FixesRunfusion/Fusion#9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix(FN-2610): add changeset for health version fix
- test(FN-2610): verify health endpoint returns real package version
- fix(FN-2610): read version from package.json in health endpoint
Update test regexps to match .modal.terminal-modal (two-class selector) and assert min-height: 100dvh on mobile. Also clean up a stale comment in TaskDetailModal.
When the soft keyboard opens on mobile, the overlay's align-items:center was vertically centering the shrunken modal, pushing the header/tabs out of view. Add a rule to switch to align-items:flex-start when --keyboard-overlap is detected.
readStoredAuthProvidersFromDisk() previously returned only the first
successfully-parsed auth file, missing providers that existed only in
fallback locations (e.g. github-copilot in ~/.pi/agent/auth.json when
another provider was in ~/.fusion/agent/auth.json). Now iterates all
candidates and merges entries with first-found-wins priority.
- Persist hidden usage-window IDs in project storage so visibility choices survive reloads
- Add hide/show actions in UsageIndicator rows plus a provider-level control to reveal hidden windows
- Style hidden window rows and usage header actions in PlanningModeModal for clear state and aligned controls
- Expand UsageIndicator and projectStorage tests to cover hide/show behavior and persistence callbacks
- Add a GitHub Copilot usage fetcher to usage collection flow
- Include copilot in provider aggregation so totals include Copilot consumption
- Map the copilot provider to the correct icon key in UsageIndicator
- Expand usage tests to cover Copilot provider parsing and aggregation behavior