Skip to content

feat(cli): inline skills via defineSkill/skillFromFile; drop dir auto-discovery#1039

Merged
buremba merged 3 commits into
mainfrom
feat/inline-skills
May 25, 2026
Merged

feat(cli): inline skills via defineSkill/skillFromFile; drop dir auto-discovery#1039
buremba merged 3 commits into
mainfrom
feat/inline-skills

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 25, 2026

What

Skills are now declared explicitly on the agent instead of being auto-discovered from magic folders. Two constructors, one object:

import { defineAgent, defineSkill, skillFromFile } from "@lobu/cli/config";

const greet = defineSkill({
  name: "greet",
  description: "Greet someone.",
  content: "Generate a warm greeting.",        // body as a string
  network: { allowed: ["api.greet.com"] },     // frontmatter as JSON
});

defineAgent({
  id: "concierge",
  skills: [greet, skillFromFile("./skills/deliveroo-order")],
});
  • defineSkill({...}) — inline: content is the body, the rest is JSON frontmatter.
  • skillFromFile("./path") — reads a SKILL.md (a dir holding one, or a .md path); the loader fills the fields from its frontmatter + body at apply time.

Both produce the same Skill; skills: [] is a flat list, deduped by name.

Why

The old model auto-walked ./skills + <agentDir>/skills and loaded everything into every agent — invisible in config, no selective control, no way to share or generate. This replaces that one magic codepath with an explicit list (the Flue-style separation: convention is gone, selection is explicit). It also matches the existing pattern for watcher reaction scripts (a path resolved post-mapper).

SOUL/IDENTITY/USER.md stay convention-loaded from the agent dir — they're a fixed bundle with nothing to choose, so explicit refs there would be pure ceremony.

How it stays small

The resolver lowers inline + file skills into the existing SkillConfig shape, so the worker, gateway, instruction-service, and DB are all unchanged. Skill frontmatter (network/nix/mcp) still merges into the agent's worker sandbox at apply time — which is why skills resolve eagerly rather than at worker boot.

Removed: loadSkillFiles / buildLocalSkills (the dir-walk). init-from-org now emits explicit skillFromFile(...) refs alongside the SKILL.md files it writes, so generated projects still round-trip.

Verification

  • bun test load-config init-from-org25 pass (new tests: inline skill, file skill, shared-by-two-agents, duplicate-name rejection, missing-file error; round-trip test exercises the new emission).
  • bun run typecheck (strict, matches Dockerfile) → clean.
  • biome check (repo config) → clean.
  • End-to-end on the real migrated example — loading examples/office-bot resolves deliveroo-order from file/, with body (2731b), judges: deliveroo, and nix: chromium all parsed from its SKILL.md frontmatter.

Note

SKILL.md bundled sibling scripts (e.g. office-bot's deliveroo.ts) were never synced to the worker (only SKILL.md content is) — that's unchanged here, and remains a separate, unimplemented capability.

Summary by CodeRabbit

  • New Features

    • Added a Skills API (inline skill definitions and file references) so agents declare skills explicitly.
  • Improvements

    • Disabled automatic folder discovery — only referenced skills are loaded.
    • Stronger validation with clearer errors for missing or duplicate skills.
    • Project/init tooling now emits and references skills via file references.
  • Documentation

    • Updated guides and references to show explicit skill declarations and conventional file locations.
  • Tests

    • Expanded CLI tests covering skill-loading and validation scenarios.

Review Change Stack

…-discovery

Skills are now declared explicitly in `defineAgent({ skills: [...] })` instead of
being auto-discovered from `./skills` + `<agentDir>/skills`. Build a skill two
ways, both producing the same object:

  - defineSkill({ name, content, network, nixPackages, mcpServers }) — inline
  - skillFromFile("./path") — reads a SKILL.md (frontmatter + body)

The apply loader resolves both into the existing SkillConfig shape (deduped by
name), so the worker, gateway, and DB are unchanged. Removes the magic dir-walk
(loadSkillFiles/buildLocalSkills); init-from-org now emits explicit
skillFromFile refs, and the office-bot example is migrated.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

This PR adds typed Skill APIs (defineSkill, skillFromFile) and a resolver that reads/parses declared skills (including SKILL.md), converts them into normalized SkillConfig entries, deduplicates by name, and integrates resolved skills into the lobu apply desired state. Bootstrapping and docs/examples are updated to emit explicit skill references.

Changes

Skills API: Explicit declarative skill configuration

Layer / File(s) Summary
Skills API definition and contracts
packages/cli/src/config/define.ts
Skill and SkillMcpServer types added; defineSkill() and skillFromFile() helpers added; Agent.skills?: Skill[] introduced and Agent.dir docs updated to remove folder auto-discovery mention.
Skill resolution and loading implementation
packages/cli/src/commands/_lib/apply/desired-state.ts
Adds resolver pipeline (readSkillFile, resolveSkill, resolveAgentSkills, skillToConfig) that parses SKILL.md frontmatter/content, normalizes network/mcp/nix fields, dedupes by name, and merges SkillConfig[] into agent artifacts. Removes old directory-scanning approach.
Config example with skillFromFile
examples/office-bot/lobu.config.ts, examples/lobu-crm/lobu.config.ts
Example configs import skillFromFile and declare agent skills arrays referencing local SKILL.md files (e.g., deliveroo-order, crm-ops).
Bootstrap/init-from-org generator updates
packages/cli/src/commands/_lib/init-from-org/bootstrap.ts
Generator now emits skillFromFile() references, tracks the import, skips system skills, writes SKILL.md files for non-system skills, and constructs agent skills arrays referencing those files.
Supporting import and comment updates
packages/cli/src/commands/_lib/apply/map-config.ts
Import ordering and comment text updated to describe new skill-entry origin; no functional changes.
Comprehensive test coverage for skill resolution
packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts
Expanded tests for skillFromFile() and defineSkill() loading, shared skills across agents, and validation cases (duplicate names, missing SKILL.md).
Documentation and guides
packages/landing/src/content/docs/*, packages/landing/src/content/docs/reference/*
Docs updated to require explicit skill declarations in lobu.config.ts, show defineSkill/skillFromFile usage, state path resolution relative to config, and remove folder auto-discovery guidance.
sequenceDiagram
  participant Config as TypedConfig
  participant Resolver as resolveAgentSkills
  participant ReadFile as readSkillFile
  participant Convert as skillToConfig
  participant State as DesiredState

  Config->>Resolver: agent.skills entries
  Resolver->>ReadFile: skillFromFile(path)
  ReadFile->>Convert: parsed SKILL.md frontmatter + content
  Resolver->>Convert: defineSkill inline entries
  Convert->>State: SkillConfig[] merged into agent artifacts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

skip-size-check

Poem

🐰 A skill to declare, a path to hold,
No more hidden in folders of old,
From defineSkill to skillFromFile bright,
Agent powers now named just right!
Config speaks clear, the scanner's retired!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introducing explicit skill declaration via defineSkill/skillFromFile and removing directory auto-discovery.
Description check ✅ Passed The description provides comprehensive coverage: What (explicit skill declarations), Why (removes auto-discovery), How (leverages existing SkillConfig shape), and Verification (test results and end-to-end validation).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/inline-skills

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 25, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 92.90780% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...kages/cli/src/commands/_lib/apply/desired-state.ts 91.93% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/_lib/apply/desired-state.ts`:
- Around line 378-380: The fallback name logic in the assignment to the local
variable name (used by skillFromFile) currently derives the name from the file
basename, causing SKILL.md to yield "SKILL" instead of the containing folder
name; update the fallback so that when the file basename (after stripping .md)
equals "SKILL" you use the parent folder's basename (via path.dirname +
basename) as the fallback, otherwise keep using the file basename; preserve
precedence of nameOverride and frontmatter?.name and only change the final
fallback used when frontmatter?.name is missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1ee6d30-6e07-4a98-8616-835cbbd532c6

📥 Commits

Reviewing files that changed from the base of the PR and between 846d173 and 7ff94f8.

📒 Files selected for processing (6)
  • examples/office-bot/lobu.config.ts
  • packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts
  • packages/cli/src/commands/_lib/apply/desired-state.ts
  • packages/cli/src/commands/_lib/apply/map-config.ts
  • packages/cli/src/commands/_lib/init-from-org/bootstrap.ts
  • packages/cli/src/config/define.ts

Comment on lines +378 to +380
const name =
nameOverride ?? frontmatter?.name ?? basename(abs.replace(/\.md$/, ""));
return { name, content: body, ...(frontmatter ? { fm: frontmatter } : {}) };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix fallback name inference for skillFromFile(".../SKILL.md").

At Line 379, the fallback infers SKILL from the filename, but the contract says fallback should come from the folder name when frontmatter name is missing. This can create false duplicate-name errors across multiple SKILL.md files.

Proposed fix
-import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
+import {
+  basename,
+  dirname,
+  isAbsolute,
+  join,
+  relative,
+  resolve,
+  sep,
+} from "node:path";
@@
-  const name =
-    nameOverride ?? frontmatter?.name ?? basename(abs.replace(/\.md$/, ""));
+  const inferredName = abs.endsWith(".md")
+    ? basename(abs, ".md").toLowerCase() === "skill"
+      ? basename(dirname(abs))
+      : basename(abs, ".md")
+    : basename(abs);
+  const name = nameOverride ?? frontmatter?.name ?? inferredName;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/_lib/apply/desired-state.ts` around lines 378 -
380, The fallback name logic in the assignment to the local variable name (used
by skillFromFile) currently derives the name from the file basename, causing
SKILL.md to yield "SKILL" instead of the containing folder name; update the
fallback so that when the file basename (after stripping .md) equals "SKILL" you
use the parent folder's basename (via path.dirname + basename) as the fallback,
otherwise keep using the file basename; preserve precedence of nameOverride and
frontmatter?.name and only change the final fallback used when frontmatter?.name
is missing.

@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 25, 2026

bug_free 62, simplicity 78, slop 6, bugs 1, 0 blockers

Typecheck/unit passed. [env] Integration failed because DATABASE_URL points at unsafe database "postgres", not a test DB. Explored inline defineSkill with MCP headers/oauth; loadDesiredStateFromConfig produced only url/type, dropping auth fields.

Suggested fixes

File Line Change
packages/cli/src/commands/_lib/apply/desired-state.ts 345 Preserve the full supported skill MCP server shape when converting defineSkill/skillFromFile entries, including headers, oauth, inputs/name, and collect secret/$VAR refs for inline skill MCP credentials.
packages/cli/src/commands/_lib/apply/map-config.ts 268 When merging skill MCP servers into agent settings, copy supported fields beyond url/type/command/args, especially headers and oauth, so inline skill MCP auth is not silently dropped.
Full verdict JSON
{
  "bug_free_confidence": 62,
  "bugs": 1,
  "slop": 6,
  "simplicity": 78,
  "blockers": [],
  "change_type": "feat",
  "behavior_change_risk": "medium",
  "tests_adequate": false,
  "suggested_fixes": [
    {
      "file": "packages/cli/src/commands/_lib/apply/desired-state.ts",
      "line": 345,
      "change": "Preserve the full supported skill MCP server shape when converting defineSkill/skillFromFile entries, including headers, oauth, inputs/name, and collect secret/$VAR refs for inline skill MCP credentials."
    },
    {
      "file": "packages/cli/src/commands/_lib/apply/map-config.ts",
      "line": 268,
      "change": "When merging skill MCP servers into agent settings, copy supported fields beyond url/type/command/args, especially headers and oauth, so inline skill MCP auth is not silently dropped."
    }
  ],
  "notes": "Typecheck/unit passed. [env] Integration failed because DATABASE_URL points at unsafe database \"postgres\", not a test DB. Explored inline defineSkill with MCP headers/oauth; loadDesiredStateFromConfig produced only url/type, dropping auth fields.",
  "categories": {
    "src": 372,
    "tests": 119,
    "docs": 63,
    "config": 4,
    "deps": 0,
    "migrations": 0,
    "ci": 0,
    "generated": 0
  }
}

Local review gate — branch protection can require the pi-review commit status. See docs/REVIEW_SCHEMA.md.

buremba added 2 commits May 25, 2026 13:01
Address review: examples/lobu-crm relied on the removed skill auto-walk and
silently lost its crm-ops skill — migrate it to skillFromFile like office-bot.
Update the docs that described folder auto-discovery (skills.mdx, skill-md.md,
lobu-config.md, agent-prompts.md) to the explicit defineSkill/skillFromFile
workflow.
defineSkill accepted the full agent McpServer (headers/oauth/env), but the skill
MCP pipeline only ever wires url/type/command/args (true for the SKILL.md
frontmatter path too), so auth fields were silently dropped. Introduce a narrow
SkillMcpServer authoring type so the API no longer promises fields it drops;
servers needing auth belong on the agent's mcpServers. Adds a merge test.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts (1)

236-264: ⚡ Quick win

Add a coverage case for type: "streamable-http" in skill MCP merge

Given SkillMcpServer.type includes "streamable-http", this test should also validate that variant is preserved in loaded agent settings. It will guard the public contract and catch silent type dropping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts` around
lines 236 - 264, Update the test that asserts MCP server merging to include the
"streamable-http" variant: when creating the inline skill via defineSkill (the
`api` constant) add an MCP entry whose type is "streamable-http" (or change the
existing mcpServers entry to use "streamable-http"), then call
loadDesiredStateFromConfig and assert against
state.agents[0].settings.mcpServers (the `mcp` variable) that the entry
preserves type: "streamable-http" along with its url using the existing expect
assertion pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/config/define.ts`:
- Around line 311-316: SkillMcpServer.type currently includes "streamable-http"
but the resolver that builds SkillConfigEntry (in the desired-state resolution
code) narrows only to "sse" | "stdio", losing "streamable-http"; either remove
"streamable-http" from the public SkillMcpServer.type or update the resolver and
SkillConfigEntry handling to preserve it end-to-end: in practice, update the
type union for SkillConfigEntry to include "streamable-http", adjust the
narrowing/merge logic in the function that converts SkillMcpServer ->
SkillConfigEntry (the resolver in desired-state.ts) to accept and pass through
"streamable-http", and ensure any merging/validation code copies relevant fields
(url/command/args) for that variant instead of dropping it.

---

Nitpick comments:
In `@packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts`:
- Around line 236-264: Update the test that asserts MCP server merging to
include the "streamable-http" variant: when creating the inline skill via
defineSkill (the `api` constant) add an MCP entry whose type is
"streamable-http" (or change the existing mcpServers entry to use
"streamable-http"), then call loadDesiredStateFromConfig and assert against
state.agents[0].settings.mcpServers (the `mcp` variable) that the entry
preserves type: "streamable-http" along with its url using the existing expect
assertion pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 94a51136-d66d-4424-a707-ef9b3bb665c4

📥 Commits

Reviewing files that changed from the base of the PR and between cf48f15 and fad5431.

📒 Files selected for processing (2)
  • packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts
  • packages/cli/src/config/define.ts

Comment on lines +311 to +316
export interface SkillMcpServer {
url?: string;
command?: string;
args?: string[];
type?: "sse" | "streamable-http" | "stdio";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

SkillMcpServer.type currently over-promises a mode that is dropped at resolution

SkillMcpServer allows "streamable-http", but the resolver path in packages/cli/src/commands/_lib/apply/desired-state.ts currently narrows to "sse" | "stdio" when building SkillConfigEntry. That silently discards "streamable-http" skills at apply time.

Please align both layers: either remove "streamable-http" from this public type, or preserve it end-to-end in skill resolution/merge.

Also applies to: 349-353

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/config/define.ts` around lines 311 - 316,
SkillMcpServer.type currently includes "streamable-http" but the resolver that
builds SkillConfigEntry (in the desired-state resolution code) narrows only to
"sse" | "stdio", losing "streamable-http"; either remove "streamable-http" from
the public SkillMcpServer.type or update the resolver and SkillConfigEntry
handling to preserve it end-to-end: in practice, update the type union for
SkillConfigEntry to include "streamable-http", adjust the narrowing/merge logic
in the function that converts SkillMcpServer -> SkillConfigEntry (the resolver
in desired-state.ts) to accept and pass through "streamable-http", and ensure
any merging/validation code copies relevant fields (url/command/args) for that
variant instead of dropping it.

@buremba buremba merged commit 5e488ce into main May 25, 2026
21 of 22 checks passed
@buremba buremba deleted the feat/inline-skills branch May 25, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants