Teachable Skills (teach-by-telling)
Teachable Skills (teach-by-telling)
Status: shipped (2026-07-04). Canonical status: PROJECT_STATUS.md.
Problem
Pace’s capability tiers include a Tier-5 goal: capture intent, not pixels, and re-ground via the model on replay. Two halves existed separately, both inadequate:
record_flow(Flows tab) teaches by demonstration but captures AX steps and replays them verbatim — brittle to any UI change. Pixel tier..skill.mdskills (PaceSkillLoader) are the intent layer — NL steps the planner re-grounds each run — but could not be created. Only hand-authored files in~/Library/Application Support/Pace/skills/. The voice grammar was list/run/install only (installwas a no-op that just confirmed a bundled skill exists), one bundled skill shipped, and the Skills tab showed something else entirely (built-in tools + MCP servers), so taught skills were invisible.
What shipped
Users teach Pace a skill by describing it in natural language — spoken or typed — fully on-device. Example:
“Pace, learn a skill. When I say ‘start my day’, open Notes, make a standup note, then open Slack.” → saved
start-my-day.skill.md(3 NL steps) → “run start my day” executes it through the normal planner loop, re-grounding the UI each run.
Modality chosen: teach-by-telling (lowest risk, on-device). Teach-by-
demonstration (generalize record_flow AX steps → NL skill) is a deferred
fast-follow on the same foundation.
Design
Zero new source files — the pure, testable core lives in PaceSkillLoader; the
one async model call lives where a planner already exists.
Skill writer — PaceSkillLoader.swift
serialize(_:)— inverse ofparse, round-trips (parse∘serialize = identity).save/deleteUserSkill/listUserSkills— user-dir only; bundled skills never touched. Atomic temp-file + rename, mirroringPaceFlowStore.writeAtomicallyandPaceMCPServerCatalog.atomicallyWriteMCPServers. Slug reusesPaceFlowStore.slug(for:).
NL → skill
skillStructuringSystemPrompt+skillFromStructuredJSON— a privacy-pinned LOCAL planner (BuddyPlannerClientFactory.makeLocalOnlyPlannerForPrivacyPinnedFeatures(), the same pin meeting-notes uses — teaching stays on-device even for cloud-tier users) structures the description into{name, trigger, steps, notes}. Lenient decode: strips markdown fences, grabs the outermost{…}, rejects empty steps.structureSkillDeterministically— no-model fallback that splits on connectives (“ then “, “, “, “; “, “. “) and pulls a “when I say …,” clause out of the front. Runs when the planner is unavailable or returns junk, so teaching never hard-fails.
Voice — PaceAutomationCommandParser.swift
PaceSkillCommand.create(rawDescription:); the parser matches “teach/learn/ create a skill …” (anchored, case-insensitive) and captures the rest. Checked before list/install/run so a create utterance (“teach a skill that lists…”) isn’t swallowed. No new routing slot — the existing dispatch atCompanionManager+AgentLoop.swiftalready runs this parser.
Create handler — CompanionManager.handleTeachSkillCommand
Local planner → skillFromStructuredJSON → deterministic fallback → save →
spoken confirmation (“saved X with N steps — say run X anytime”). Fails soft.
Settings — PaceSkillsView.swift
Resolves the naming collision: a “Your skills” section lists taught skills
(delete per row) with an inline “Teach a skill” typed form
(skillFromForm, deterministic, no model — the typed sibling of the voice path),
plus a read-only “Built-in skills” subsection. Skills tab now = “everything Pace
can do, including what you taught it.”
Privacy
Structuring runs on the privacy-pinned local planner — never the CLI bridge or Direct API tier — so a taught skill never leaves the Mac, consistent with the on-device moat.
Deferred
- Teach by demonstration → generalize — reuse
record_flowAX capture, convert steps to NL intent, save as a skill. Builds onPaceSkillLoader.save+ the Skills-tab section shipped here. requiredPreferenceson taught skills — default[]; bundled-skill preference gating viaPaceLocalMemoryKeyis unchanged.
Tests
leanring-buddyTests/PaceSkillLoaderTests.swift: serialize round-trip, save/list/
delete + overwrite (temp dir), skillFromStructuredJSON (clean/fenced/prose/empty/
fallback/garbage), deterministic splitter, skillFromForm, and parser create +
list/install/run regression. Run via bash scripts/test-pace.sh PaceSkillLoaderTests.