COMPLETE a skill into an agent definition
A SKILL.md is procedural knowledge injected into the caller's turn. A subagent
is a separate worker with its own context, persona, tools, model, and a defined
return value. An agent is mostly a skill plus a thin "extras" envelope.
COMPLETE synthesizes that envelope — mechanically, no LLM needed.
The two extras that actually matter:
- the persona (system prompt / identity), and
- the return contract (a JSON Schema so a manager can consume the output).
coact keeps the skill on disk as the single source of truth — the agent references the skill by name and never copies its body.
Look before you leap: plan first
plan_completion is a dry-run. It returns the proposed agent plus per-field
provenance and warnings — every synthesized field and where it came from:
from coact import plan_completion
plan = plan_completion(".claude/skills/ux-analyst")
print(plan.render()) # name←skill, tools←inferred/coact:, model←policy, ...
plan.agent # the AgentDefinition it would produce
plan.warnings # e.g. "tools not declared; inferred from ..."
coact plan .claude/skills/ux-analyst # same dry-run, in the terminal
When you're happy:
from coact import complete, emit_agent
agent = complete(".claude/skills/ux-analyst") # -> AgentDefinition
print(emit_agent(agent, "claude-agents-md")) # the .md, as text
emit_agent(agent, "claude-agents-md", dest=".claude/agents/") # write it
coact complete .claude/skills/ux-analyst --dest .claude/agents
source accepts a Skill, a path to a skill dir / SKILL.md, or a key/name
resolvable in the local store or .claude/skills/.
Make the lift reproducible: the coact: block
The standard name / description frontmatter doesn't carry tools, model, MCP,
or a return contract. Add an additive, optional coact: block (every other
tool ignores it). When present it wins over policy; when absent coact infers
and reports what it guessed.
---
name: ux-analyst
description: Analyze a captured UX evidence bundle for usability issues.
coact:
tools: [Read, Grep, Glob] # else inferred from the skill (a warning is recorded)
disallowed_tools: [Bash] # narrow the allowlist
model: sonnet # sonnet | opus | haiku | inherit (else policy routes)
memory: project # user | project | local (opt-in)
permission_mode: default
skills: [shared-evidence] # extra skills the agent references (point-don't-copy)
consumes: evidence_bundle # input contract name (signals interdependence)
returns:
schema_ref: ov.schemas:UxFindings # XOR json_schema — resolved to canonical JSON Schema now
description: Usability findings for the bundle
mcp:
- module: ov.analyzers # python tools, exposed at REALIZE-time (backend='mcp')
functions: [score_contrast, find_tap_targets]
persona: |
You are a meticulous UX analyst... # else a template persona is synthesized
---
Validate it (also runs under skill validate once coact is imported):
from coact import validate_coact_block, parse_coact_meta
validate_coact_block(skill_frontmatter_dict) # [] = valid/absent
parse_coact_meta(".claude/skills/ux-analyst") # -> CoactMeta
Key behaviors to remember
- No LLM by default. The whole path is mechanical. To draft a richer
persona, opt in explicitly:
complete(skill, llm=...)(acallable(str)->str, anawStepConfig, or a model name). Withoutllm=, nothing calls a model. - Defaults are sensible & reported. Tools/model/memory come from the
coact:block if declared, else from the injectedCompletionPolicy(from coact import default_policy) — and provenance tells you which. - schema_ref is resolved at completion time to a canonical JSON Schema; an unresolvable ref is recorded as a warning, never a crash.
- mcp tools are not embedded in the definition — they're exposed at
realize-time (
backend="mcp");planwarns you when a skill declares them.
Next
coact-realize— turn the completed definition into something that runs.coact-analyze—diffa skill against its agent to audit the extras.