Repository navigation
feat: skills — SKILL.md files loaded on demand (#252) - #302
Merged
Merged
Conversation
Port OpenCode's MIT-licensed skill mechanism to Python: discover skills/**/SKILL.md under the workspace and the OpenBot state dir, put only names/descriptions in the system prompt, add a per-bot load_skill tool that returns a skill's body + base dir + sibling files, and per-bot enablement (bot_profiles.skill_names, migration 0022) validated against discoverable ids via the API. Frontend: GET /tools/skills-backed skill picker in the bot editor; GET /api/v1/tools/skills lists discovered skills. Also: busy indicator text 'busy, waiting...' -> 'waiting...' (label + aria-label, tests updated).
Making skill_names required on the Bot type broke three other test factories (botActivity, BotPage, SaveFeedback) that construct a full Bot; my local tsc check was blinded by an incremental cache, CI caught it.
# Conflicts: # frontend/src/api/client.ts
regnull
commented
Oct 18, 2026
regnull
left a comment
Owner
Author
There was a problem hiding this comment.
PASS — Ready to merge (COMMENTED verdict because self-approval is blocked on the shared account).
Verified against the diff at 390d46a, all priority targets:
- client.ts merge resolution ✅ — line 1 import union retains BOTH
BotScratchpadandSkillInfo; diffed against origin/main, the only client.ts changes are the import line +listSkills. Scratchpad type/endpoint (from #303) intact. skill_namesrequired onBot✅ — everyBotliteral on the PR head updated (botActivity.test.ts,BotEditorPage,BotPage,SaveFeedback); grepped all frontend test files that constructBot(viamemory_enabled, also required) — none missed.- Malformed front matter non-fatal ✅ —
discover_skillsswallowsSkillError/OSError per file and continues; pinned by 8+ parametrized tests (broken YAML, non-mapping, missing name/description, bad ids, overlong fields — all leave other skills discoverable). - Dual gate (grant + enablement) ✅ — runner adds skills to the prompt only when
load_skillis granted ANDskill_namesnon-empty (enabled_skillsreturns [] otherwise); the tool re-filters by the enabled set per call and errors for disabled/unknown skills (pinned bytest_load_skill_does_not_offer_disabled_skills). API rejects unknown names with 422 (pinned). - Catalog-wide grant risk ✅ —
test_selectable_namesupdated withload_skill; seeded-bot tool-list asserts intest_token_efficiency.pycorrectly unchanged (demo bots don't get the tool;SYNCED_FIELDSsyncsskill_nameslike the other fields). - Report-vs-diff ✅ — both backend cores are full new-file implementations in the diff (170 + 103 lines), no phantom pieces.
- MIT credit ✅ — present in both new file docstrings.
Migration 0022 (JSON, NOT NULL, server_default '[]'), API surface (create/update/catalog-install), prompt wiring, editor UI and busy-indicator rename (label + aria-label, 3 tests updated) all check out.
test_workspace_roots_shadow_the_state_dir(backend/tests/test_skills.py) is vacuous:assert skills.id != "Deep Review"passes regardless of which root wins because the id is always "deep". Suggest asserting the winningname(== "Deep Review"). The implementation itself is correct (setdefault, first root wins).0022_bot_skill_names.pydocstring says "Revision ID: 0021 / Revises: 0020" while the actual constants are 0022/0021.seed.pydropped to a single blank line betweengrant_scratchpad_to_existing_botsandsync_demo_bots(cosmetic; CI lint already green).
OpenBot - @reviewer
# Conflicts: # backend/openbot/runtime/prompt.py # backend/openbot/runtime/runner.py
This was referenced Oct 18, 2026
regnull
added a commit
that referenced
this pull request
Oct 18, 2026
- test_skills: replace the vacuous name-vs-id assertion in test_workspace_roots_shadow_the_state_dir with a real check that the workspace-root copy shadows the state-dir one - migration 0022: correct the stale revision docstring (0021/0020 -> 0022/0021) - seed.py: restore two blank lines dropped by the skills commit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Skills: SKILL.md files loaded on demand (issue #252)
Ports OpenCode's skill mechanism to Python (OpenCode is MIT-licensed — credited in the
docstrings of
backend/openbot/skills.pyandbackend/openbot/tools/builtin/skills.py).How it works
discover_skills()walksskills/**/SKILL.md(orskills/<id>.md) under theworkspace root and OpenBot's own state directory (
.openbot/skills), skipping junk dirs(
.git,node_modules,__pycache__, …), capped at 64 skills. Workspace skills shadow same-idskills from the state directory.
nameanddescription; malformedfront matter, bad ids (letter/digit-start, ≤64 chars) or overlong fields make a skill unusable
(
SkillError) but never break the others. Multiline YAML scalars are folded into one line.# Skillssection), via theexisting
build_system_prompt_partsmachinery; a bot with no skills costs nothing.load_skill(name)tool — returns the skill's body inside an OpenCode-style<skill_content>envelope plus the skill's base directory and its sibling files (capped at 10,for use with
read_file). Matches by id or display name, case-insensitively.skill_namescolumn onBotProfile(migration 0022), syncedthrough the catalog/API like
tool_names; a bot must both grant theload_skilltool andenable the skill for either to take effect (skills only reach the prompt when the tool is
granted). The API rejects unknown skill names with 422.
Files changed
Backend:
openbot/skills.py(discovery/parsing/section),openbot/tools/builtin/skills.py(the tool),
runtime/runner.py+runtime/prompt.py(prompt wiring),db/models.py+db/migrations/versions/0022_bot_skill_names.py,api/schemas.py,api/bots.py,api/tool_validation.py(known_skill_names),api/tools.py(newGET /tools/skillsendpoint for the editor),
bots/catalog.py,seed.py(synced fields),config.py(
state_dirproperty),pyproject.toml(pyyaml).Frontend:
SkillInfotype,Api.listSkills, a Skills card inBotEditorPage(per-bottoggles + empty-state hint),
skill_nameson theBottype.Also in this PR (task 2): the bot busy indicator now reads "waiting…" instead of
"busy, waiting…" (
BotBusyIndicator.tsxlabel + aria-label, with the three tests that pinnedthe old string updated).
Tests
backend/tests/test_skills.py(28 tests): discovery layouts and shadowing, junk-dir pruning,the 64-skill cap, front-matter validation (malformed YAML, missing fields, bad ids, overlong
fields), the prompt section (ids/descriptions only), per-bot enablement by id and display name,
load_skillhappy path/errors/no-disabled-leaks, the sibling-file cap, runner prompt wiring(skills appear only when
load_skillis granted), and API validation (unknown-skill 422,GET /tools/skills). Plus the editor test that toggles a skill and asserts the saved payload.Local: backend
uv run pytest -q→ 815 passed;uv run ruff check .clean. Frontendpnpm test→ 347 passed (57 files),tsc --noEmitclean,pnpm lintexit 0 (one pre-existingwarning in
ThreadPage.tsx, untouched).