{"slug": "sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents", "title": "SoPhi Harness — From Inert Files to Self-Writing Skills: Evolving an AI Agent’s Skill System", "summary": "Sophi's sophi-skills module shipped as an inert Markdown/YAML skill store with a SkillLoader and SkillRegistry that had no consumer for weeks, until a SkillTool was added to let the model pull a skill's instructions into context on demand. The module's pom.xml depends only on kotlin-stdlib, a YAML parser, and sophi-versioning — no sophi-core, Tool interface, or coroutines — so SkillTool could not live inside sophi-skills itself. SkillRegistry layers a global directory under a project directory, with project entries winning on ID collision and malformed cards skipped via runCatching.", "body_md": "A general contractor doesn’t wait for a family to pick out a stove before running the gas line to the kitchen. Rough-in work happens before there’s a tenant: the pipe stubs out of the floor where a sink will eventually sit, built to spec and completely inert, because the fixtures that will actually use any of it haven’t been chosen yet. You build for a future occupant you can describe in general terms but haven’t met.\n\nsophi-skills Shipped at the very beginning during the skeleton creation as exactly this kind of rough-in work: a place to keep instructions nobody was reading yet. The module did nothing useful the day it landed. It was finished months later by something else entirely — and the gap between \"stored\" and \"read\" is where this story actually lives.\n\nA skill in Sophi is a single Markdown file with YAML frontmatter — a recipe card, one file per dish:\n\n```\n---  title: \"Deploy runbook\"  description: \"How to deploy this service safely\"  tags: [ops, deploy]  ---  Steps to deploy...\ndata class Skill(  val metadata: SkillMetadata,    val body: String,    val source: Path  )\n```\n\nSkillLoader reads a whole directory of these into memory eagerly — every file, every byte, parsed at startup, sorted by title. SkillRegistry layers a global directory underneath a project directory, project entries winning on ID collision, tolerant of a malformed card \n\nrunCatching { loader.loadFile(file) }.getOrNull() — one bad recipe doesn't spoil the box). None of that is \"lazy\" in the way the module's own description implies. What's actually lazy is what happens *after* loading: a skill's body sits parked in a Map<String, Skill> doing nothing until something specifically asks for it. Nobody reads a recipe box front to back before deciding what's for dinner — you pull one card, the one you need, and the rest stay exactly where they were.\n\nHere’s the detail that makes the analogy earn its keep instead of just decorating the prose: sophi-skills's pom.xml depends on kotlin-stdlib, a YAML parser, and — since skill versions needed somewhere durable to live — sophi-versioning, itself nothing more than kotlin-stdlib plus a storage abstraction. No sophi-core. No Tool interface. No coroutines, anywhere in that chain. A recipe card doesn't need a stove to exist — it's paper, and Sophi's skill format is the software equivalent, a pure parse-Markdown-into-data-classes module that could sit in a completely different project and still make sense on its own terms. Which meant, for a while, it had no way to get *into* the kitchen at all.\n\nSkills landed as a loadable data structure with genuinely nothing consuming it — the SkillLoader/SkillRegistry pair shipped, worked, had tests, and had no caller anywhere for weeks. The recipe box existed. Nobody had told the cook it was in the pantry. What eventually gave it a consumer was a plain Tool that lets the model pull a card off the shelf itself — and where that Tool could live turned out to be its own small puzzle:\n\n```\nclass SkillTool(  private val registry: SkillRegistry,   private val topK: Int? = null) : Tool {    override val name = \"skill\"      override val description: String = \"Load a skill's instructions into context. Available skills:\\n\"     + registry.topLevel().let {         all -> topK?.let { all.take(it) } ?: all       }.joinToString(\"\\n\") {         (id, skill) -> \"- $id: ${skill.metadata.description}\"       }      override fun riskLevel(argumentsJson: String) = RiskLevel.SAFE    override suspend fun execute(argumentsJson: String): String {    val skillName = args?.name ?: return \"Error: missing 'name' argument\"    val skill = registry.get(skillName) ?: return \"Error: skill not found: $skillName\"    val children = registry.childrenOf(skillName)    if (children.isEmpty()) return skill.body    return skill.body + \"\\n\\nAvailable in this domain:\\n\"       + children.joinToString(\"\\n\") { (id, child) -> \"- $id: ${child.metadata.description}\" }      }}\n```\n\nSkillTool couldn't live in sophi-skills — the whole point of the previous section was that sophi-skills never learns what a Tool even is. It couldn't stay parked in sophi-cli forever either: [ADR-028](https://github.com/shukriev/SoPhi/blob/main/doc/adr/ADR-028-shared-tool-wiring.md) (August 25th) gave sophi-companion the same tool set as the terminal, and a class only sophi-cli could see couldn't serve a second host.\n\nRegistered conditionally — if the pantry's empty, there's no point offering a tool that does nothing:\n\n```\nif (skillRegistry.topLevel().isNotEmpty()) {  registry.register(SkillTool(skillRegistry, topK = harnessConfig?.topKSkills))}\n```\n\nand a second front door, /skill <id>, that skips asking the model entirely and slides the card straight into the conversation:\n\n```\nsession.append(EntryRole.TOOL_RESULT, skill.body)  output(\"Injected skill: $sub\")\n```\n\nTwo ways in — the model deciding it wants a recipe mid-turn, or a human handing it one — both reaching the same shelf. Neither existed the week the shelf itself was built, and by the time the first one did, the shelf had grown tabs: SkillRegistry now distinguishes a domain's root card (topLevel()) from its members (childrenOf(domainId)) — ask SkillTool for site-github-com today and it hands back that card plus a list of every sibling card filed under it, the same box now organized as a binder instead of one flat stack of index cards.\n\nReading a card was the previous milestone’s whole job. Later, the box gained a way to fill itself:write_skill lets the agent record a new site-specific card after it works out how a website behaves — site-github-com, or a domain member like\n\nsite-maidplus-de/companies — and install_skill pulls a Claude-Code-shaped SKILL.mdin from a local path or git URL and normalizes its frontmatter to match. [ADR-030](https://github.com/shukriev/SoPhi/blob/main/doc/adr/ADR-030-skill-write-admission-gate.md), on August 27th, is what makes that safe to leave running: static content checks (a secret/credential scan, a prompt-injection-phrase scan for anything arriving from outside the codebase) block a write before a single byte lands, and neither tool got to skip the door [article-06](https://medium.com/towards-artificial-intelligence/sophi-harness-hands-that-dont-freeze-a-tool-interface-built-for-a-future-it-hadn-t-earned-yet-6edf41716352) built:\n\n```\noverride fun riskLevel(argumentsJson: String) = RiskLevel.DESTRUCTIVE\n```\n\non both — a card that changes what a future turn believes about the world doesn’t get to be SAFE just because the box holding it started out inert. install_skill goes one step further and opts out of auto mode's shortcut entirely (ruleVerdict returns HIGH_RISK unconditionally): content arriving from a git URL always gets a human's eyes on it, no exception a classifier gets to grant.\n\nA written card lands as a trial version, not a promoted one — reading it back to check it actually helped is a separate command (sophi skill verify) run on a separate day. That's deliberate, not a missing feature: running an eval suite inside the tool call would stall the turn for minutes waiting on a judgment nobody asked for yet. The verification-and-promotion machinery itself — what the eval suite scores, why it runs\n\ntwice, how a coverageWarning catches a suite that never actually exercised the new skill — is its own story, not this one.\n\nSkills shipping ahead of its own need is exactly the risk rough-in work always takes — you might build for a room that gets used differently than you guessed. It paid off twice over. First, months later, with a Tool and a slash command that needed nothing from sophi-skills itself. Then again a year after that, when the consuming side had to move buildings entirely — sophi-cli to sophi-sdk, then reused unmodified by a second host app that didn't exist when scleton was shipped — and sophi-skills's own code never noticed, because it was never depended on for anything more than Skill and SkillRegistry in the first place. Same instinct as riskLevel's boring SAFE default from [article-06](https://medium.com/towards-artificial-intelligence/sophi-harness-hands-that-dont-freeze-a-tool-interface-built-for-a-future-it-hadn-t-earned-yet-6edf41716352): build the boring, general-purpose thing, leave room, and let whoever moves in next decide what actually goes in the room — even when \"whoever\" turns out to be a module and a host\n\napp that hadn't been designed yet. sophi-extensions — the other half of the previous milestone, a different kind of rough-in entirely, wiring observers into the agent loop instead of storing instructions for it — is its own story, told in the future.\n\nThe next article in this series covers building sub-agents, or as we call it: **Calling in a subcontractor: subagents as just another Tool**\n\n*If you’re working on agent-based systems or have thoughts on the build-vs-buy divide in the JVM ecosystem, I’d love to hear your take in the comments below. If this deep dive helped clarify your own architectural choices, a few claps go a long way in helping other engineers find this series. Follow along to stay updated as we build in public.*\n\nFind the full source on [GitHub](https://github.com/shukriev/SoPhi) and connect with me on [LinkedIn](https://www.linkedin.com/in/shukri-shukriev-61b26882/).\n\n[SoPhi Harness — From Inert Files to Self-Writing Skills: Evolving an AI Agent’s Skill System](https://pub.towardsai.net/sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents-skill-system-89a2facfc9ce) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents", "canonical_source": "https://pub.towardsai.net/sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents-skill-system-89a2facfc9ce?source=rss----98111c9905da---4", "published_at": "2026-10-07 19:01:01+00:00", "updated_at": "2026-10-07 19:20:12.658306+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Sophi", "sophi-skills", "SkillLoader", "SkillRegistry", "SkillTool", "sophi-versioning", "kotlin-stdlib", "Skill"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents", "markdown": "https://wpnews.pro/news/sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents.md", "text": "https://wpnews.pro/news/sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents.txt", "jsonld": "https://wpnews.pro/news/sophi-harness-from-inert-files-to-self-writing-skills-evolving-an-ai-agents.jsonld"}}