Skills previously lived as flat `<slug>.md` files injected directly into
the user prompt, which bypassed Claude Code CLI's native discovery entirely.
Switch to the CLI's expected `<slug>/SKILL.md` layout so the SDK can surface
each skill's frontmatter to the model and let it call `Skill(slug)` lazily
on its own — without having to eagerly inject every skill body every turn.
Key changes:
- One-shot startup migration moves existing flat files into the directory
layout (idempotent, crash-safe, preserves existing dir if already migrated)
- `get_installed_slugs()` with module-level cache (keyed by SKILLS_DIR) so
the per-turn allowlist query is effectively free on the hot path
- `_ensure_frontmatter()` stamps a valid YAML block on every write so the
CLI listing never silently drops a skill with missing frontmatter
- `_resolve_sdk_skill_allowlist()` replaces the old `skills=[]` kill-switch:
passes only user-installed slugs to the SDK, filtering out bundled Claude
Code skills (/init, /review, etc.) by not including them, and dropping
manually-attached skills that are already injected into the user message
- CRUD endpoints and tests updated to the new directory layout throughout
Pass `skills=[]` to ClaudeAgentOptions so the SDK's built-in plugin
skills (/init, /review, /security-review, /simplify, /loop, /schedule,
/update-config, /keybindings-help, /fewer-permission-prompts,
/claude-api) are hidden from the model and rejected by the Skill tool.
These skills are inappropriate in OpenSwarm: half mutate ~/.claude
config files (settings.json, keybindings.json) that OpenSwarm doesn't
read, and the rest expose slash commands the backend never intercepts —
causing the model to falsely claim capabilities it can't actually use.
OpenSwarm's own skills system injects skill content directly into the
user prompt via _resolve_attached_skills, bypassing the Skill tool
entirely, so user-attached skills are unaffected.
Also adds a regression test that pins the `skills=[]` assignment so it
can't be silently dropped in a future refactor.
Three submit_* helpers and one DEPRECATED env builder had no callers
outside their own tests — confirmed via dead-code scan + repo-wide
grep across backend/, frontend/, electron/, scripts/. Drops 50 LOC
with zero behavioral impact (full backend test suite still green at
1096 passed).
- backend/apps/service/client.py: drop submit_state, submit_session_close,
submit_diagnostic. None were called in production; remaining wire shape
(update_identity's submit("state", {"identity": ...})) is preserved.
- backend/apps/settings/credentials.py: drop get_agent_sdk_env. Was
self-documented as DEPRECATED in favor of create_provider() /
get_anthropic_client*; no callers anywhere.
- backend/tests/test_service.py: drop the two test_legacy_submit_*
cases that were the only references keeping the helpers above the
vulture threshold.
Co-authored-by: Cursor <cursoragent@cursor.com>
Each removal verified by checking actual production callers (frontend,
electron, internal HTTP, MCP-server subprocesses) — not just test
references. Symbols whose only callers were tests are removed along
with those tests.
Production removals (~390 LOC):
- backend/main.py
- websocket_session: drop `agent:edit_message` WS branch. Frontend
only ever uses HTTP `POST /api/agents/sessions/{id}/edit_message`
(frontend/src/shared/state/agentsSlice.ts); nothing on the wire
sends a WS message of this type.
- backend/apps/agents/agent_manager.py
- AgentManager._build_connected_tools_context (~80 LOC): zero call
sites in production; the connected-tools system-prompt context is
built inline in _compose_system_prompt now.
- AgentManager._approx_tokens / _summarize_message_block: pure
helpers whose only callers were tests. The compaction path uses
LLM-driven _maybe_compact instead.
- backend/apps/agents/browser_agent.py
- clear_browser_history: only used by tests. _browser_history is
pruned via the size cap inline.
- MODEL_MAP constant: never read.
- backend/apps/agents/mcp_preflight.py
- DISCOVERY_SCAFFOLDING (~25-line system-prompt block): defined but
never appended anywhere. The header comment described an intended
use that the codebase no longer has.
- backend/apps/agents/providers/registry.py
- thinking_params_for, _is_9router_available, OPENROUTER_BASE_URL,
get_context_window: zero callers in production. Thinking-params
routing is done by the provider classes directly; 9Router presence
is detected at request time; context-window numbers are stamped
onto sessions from BUILTIN_MODELS at launch.
- backend/apps/agents/tools/{base,web}.py
- BaseTool.get_schema (abstract) + WebSearchTool/WebFetchTool
overrides: production code in backend/apps/web/web.py instantiates
these tools and only calls .execute(); the JSON-schema lives in
the HTTP wrapper, not on the tool class.
- backend/apps/outputs/outputs.py
- _resolve_model + MODEL_MAP: tests-only.
- load_output: docstring claimed it was a public helper for "other
modules" but no module imported it.
- backend/apps/service/client.py
- set_user_id, the _user_id module global, and the dead cache short-
circuit in _get_user_id: setter was tests-only. _get_user_id now
reads user_email directly from settings on every call.
- backend/apps/settings/credentials.py
- get_provider_credentials: zero callers. The sibling get_agent_sdk_env
is kept (it has the explicit "Legacy helpers" keep-comment).
Test updates:
- test_agent_manager_unit.py: drop _approx_tokens / _summarize_message_block
cases (5 tests), update module docstring index.
- test_browser_agent_unit.py: drop clear_browser_history cases (2 tests)
and the unused _Boom helper class in the repr-fallback test.
- test_outputs_unit.py: drop _resolve_model / load_output cases
(4 tests), update docstring + import list.
- test_v2_invariants.py: drop get_context_window tests + get_schema
assertions on web tools (kept name + BaseTool inheritance checks).
- test_service.py: rewrite the 4 set_user_id-driven tests to drive
user_id through settings.user_email instead, so _get_user_id's live
envelope-stamping path stays covered.
Verification:
- ruff --select F401,F811,F841 backend/ → clean.
- pytest backend/tests/ → 1167 passed, 1 deselected (pre-existing
sandbox git test, unrelated). No tests dropped silently — every
deletion is paired with the corresponding test removal/rewrite.
- Dead-code scan re-run: dead WS events 1→0, Tier-2 high-confidence
14→11 (residue is SDK-callback `context` params + Pydantic `cls`
validators — both false positives vulture can't see through),
vulture total 165→145.
Total diff: -565 / +34 LOC across 15 files.
Co-authored-by: Cursor <cursoragent@cursor.com>
Auto-fixed 52 ruff F401 findings (unused imports) across 22 files
in backend/ and backend/tests/. Manually resolved 8 F841 unused
locals that ruff flagged as unsafe-fix:
- agent_manager.resume_session: drop dead hours_since_closed block.
- main.py mcp-meta + outputs-meta activate handlers: drop dead
reason = body.get("reason") binding (server ignores the field).
- dashboards.seed_demo, tools_lib.m365_device_login: keep _load(...)
call for its 404 side-effect, drop unused binding, add intent
comment.
- outputs.auto_run_output: keep `import anthropic` as availability
probe, mark with `# noqa: F401` and explanation.
- dead_code_scan._extract_ws_event_branches: drop vestigial
ws_handler_lines set (never written or read).
- test_browser_agent_unit.test_hash_tool_call_falls_back_to_repr:
drop the unused _Boom class+instance (the actual self-referential
bait is bad_input/bad_result; _Boom was never passed to the
function under test).
Result: 1184/1184 backend tests pass (1 deselected: pre-existing
sandbox-only git test). ruff --select F401,F811,F841 backend/ now
clean (was 60 findings).
Co-authored-by: Cursor <cursoragent@cursor.com>
Commit backend/scripts/dead_code_scan.py so the tooling that produced
the recent dead-code removals is preserved in the repo.
Update .gitignore to stop ignoring the script itself while keeping the
generated .dead-code-scan/ output directory ignored.
Co-authored-by: Cursor <cursoragent@cursor.com>
Verified each had no production caller (frontend, electron, or
internal backend) — only test references kept them looking alive
to coverage tools. Drops ~140 LOC of route handlers + helpers.
- POST /api/outputs/vibe-code: never wired up; frontend has no
vibe-code UI. Also drops VibeCodeRequest model and
VIBE_CODE_SYSTEM_PROMPT.
- GET /api/service/cost-breakdown: frontend Usage page reads
/usage-summary, which already returns by_model/by_provider.
- GET /api/service/status: zero callers; was placeholder.
- GET /api/service/spool/count: debug-only, no UI surface.
- GET /api/settings/default-system-prompt: frontend defines its
own DEFAULT_SYSTEM_PROMPT in settingsSlice.ts and never fetches
the backend constant.
- POST /api/browser/command: sole caller (browser_mcp_server.py
subprocess) was deleted in 8286cc1; browser_agent.py now calls
ws_manager.send_browser_command directly in-process.
Tests covering the removed endpoints are dropped along with the
now-unused mock-anthropic helpers in test_api_outputs.py.
backend/apps/dashboard_layout/ was never mounted in main.py and had no
frontend/electron callers — its SubApp and endpoints were fully superseded
by backend/apps/dashboards/. backend/apps/service/models.py was an empty
placeholder with no importers. Also adds .dead-code-scan/ and the scan
script to .gitignore, and drops the stale dashboard_layout/ entry from
the README directory tree.
Co-authored-by: Cursor <cursoragent@cursor.com>
updates instantly (rollback if server rejects), dashboard cards no longer shake neighbors during streaming, plus shared motion/loading
primitives for future consistency. Toolbar model/mode/thinking pick now writes through to the global default so it sticks across reopens.
pruned from layout state on the next render (gated on outputs loaded so the fix can't wipe valid cards during the load race), and clearing
all cards from a dashboard now actually clears its thumbnail on exit instead of leaving the last 'alive' image stuck forever.
only load when you click into them, Settings pre-loads in the background so opening it stays instant), added a safety net that shows a
friendly error card instead of blacking out the whole app, and stopped a few dashboard lists from needlessly rebuilding on every chat
update.
in sentence case; activating an MCP mid-chat now actually works instead of the model guessing at made-up tool names. Also a bunch miscelaneous ui/ux tweaks, I cant be bothered :)
completed chats no longer replays the typewriter (per-session lastSeq survives AgentChat remount so resume protocol stays at the high-water
mark instead of last_seq=0)
reasoning text" note instead of vanishing); friendlier MCP tool names (Gmail/Slack/etc with verb-derived actions); auto-collapse Anthropic
thinking pill on turn end so the answer comes first; header timer matches per-turn pill (sums in ms, rounds once); thinking pill rolls 251s
to 4m 11s; parallel tool count chips do not jitter as N grows; drop Gemini 2.5 from picker (Gemini 3 family only); aux-LLM calls route
per-model so cheap-tier auxiliary calls do not 401 on cc/ subscription routes
drop) instead of flipping to completed. Adds heartbeat (25s ping/10s pong), reconnect with infinite jittered backoff, outbound queue gated
on resume_ack, gap_detected fallback for long offlines, on-disk persistence of terminal events for post-restart recovery, and a
reconnecting connection state decoupled from session.status. 1089 backend tests covering 500 randomized disconnect scenarios + concurrent
broadcast races. Backend/WS handler does not cancel agent task on disconnect