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>
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