opsward.subagents#
Reuse a skill as a subagent: declare it in the skill, generate the twin, check the sync.
A Claude Code subagent is a Markdown file under .claude/agents/ whose frontmatter
can preload a skill (skills: [name] injects the full SKILL.md body). So a skill
that is also useful in its own context needs nothing but a thin twin that preloads it,
restricts tools, picks a model and states the reply contract. The single source of
truth stays in the skill: it opts in with spec-legal metadata keys (the Agent Skills
spec allows string pairs there, and other hosts ignore them). Quote the values so a
strict validator sees strings:
---
name: fleet-sweep
description: ...
metadata:
subagent: "true"
subagent-tools: "Bash, Read, Grep"
subagent-model: "sonnet"
subagent-reply-lines: "5"
subagent-name: "fleet-sweeper"
---
Only subagent is required; the defaults are Read, Grep, Glob, Bash, no model
(the host picks), 5 lines, and the skill’s own name. Three pure functions do the
work, so generate and maintain share one source: subagent_spec() reads
the declaration, render_subagent() renders the twin (with a marker line
carrying a hash of what it was rendered from), and twin_status() classifies
every skill/agent pair in a scan. Decision record:
misc/docs/decisions/0009-skills-declare-their-subagent-twin.md.
Functions
|
|
|
Render the twin agent file for spec; source is the skill's repo-relative path. |
|
Yield the twin agent file for every validly declared skill (missing or not). |
|
Return the twin a skill declares under |
|
The twin of a skill that is about to be written (so one |
|
Classify every declared twin and every hand-written agent that preloads a skill. |
|
Counts by state, for the diagnosis note. |
Classes
|
What a skill declared about its subagent twin, with defaults applied. |
|
One skill/agent pair as |
- class opsward.subagents.SubagentSpec(skill_name, name, description, tools, model, reply_lines)[source]#
Bases:
objectWhat a skill declared about its subagent twin, with defaults applied.
- problems()[source]#
Why this declaration cannot be rendered (empty = fine).
>>> SubagentSpec('s', '../x', '', 'Bash: all', None, 0).problems() ['no frontmatter description (the twin reuses it as its trigger)', 'subagent-name `../x` is not a valid agent name', 'subagent-reply-lines must be at least 1', 'subagent-tools must not contain `: `']
- class opsward.subagents.TwinStatus(skill, agent, state, declared=False, content='', detail='', path='')[source]#
Bases:
objectOne skill/agent pair as
twin_status()sees it.
- opsward.subagents.check_subagent_twins(sr)[source]#
maintaincheck: every twin that is not generated and current.- Return type:
- opsward.subagents.render_subagent(spec, *, source)[source]#
Render the twin agent file for spec; source is the skill’s repo-relative path.
The body is deliberately short: the skill’s full content is preloaded by the host through the
skillsfield, so the twin only adds the context contract.>>> spec = SubagentSpec('sweep', 'sweep', 'Sweep and report.', 'Bash', 'haiku', 3) >>> text = render_subagent(spec, source='.claude/skills/sweep/SKILL.md') >>> text.splitlines()[1:4] ['name: sweep', 'description: Sweep and report.', 'tools: Bash'] >>> 'skills: [sweep]' in text and 'at most 3 lines' in text True
- Return type:
- opsward.subagents.subagent_files(sr)[source]#
Yield the twin agent file for every validly declared skill (missing or not).
Callers that must not overwrite (the CLI) skip existing targets; the marker in a generated file is what
twin_status()uses to tell a stale twin from a hand-written one. Invalid and duplicate declarations yield nothing;maintainreports them.- Return type:
- opsward.subagents.subagent_spec(skill)[source]#
Return the twin a skill declares under
metadata, orNone.>>> from pathlib import Path >>> from opsward.base import SkillInfo >>> s = SkillInfo(name='sweep', path=Path('/x/sweep'), has_skill_md=True, ... description='Sweep the fleet and report.', ... frontmatter={'name': 'sweep', 'metadata': {'subagent': 'true', 'subagent-model': 'haiku'}}) >>> spec = subagent_spec(s) >>> (spec.name, spec.tools, spec.model, spec.reply_lines) ('sweep', 'Read, Grep, Glob, Bash', 'haiku', 5) >>> subagent_spec(SkillInfo(name='plain', path=Path('/x/plain'))) is None True
- Return type:
- opsward.subagents.twin_for_rendered_skill(skill_md_text, skill_md_path, root)[source]#
The twin of a skill that is about to be written (so one
generaterun creates both).>>> from pathlib import Path >>> text = '---\nname: s\ndescription: Do the thing.\nmetadata:\n subagent: "true"\n---\nbody\n' >>> gf = twin_for_rendered_skill(text, Path('/p/.claude/skills/s/SKILL.md'), Path('/p')) >>> gf.target_path.as_posix(), 'skills: [s]' in gf.content ('/p/.claude/agents/s.md', True)
- Return type:
- opsward.subagents.twin_status(sr)[source]#
Classify every declared twin and every hand-written agent that preloads a skill.
invalid: the declaration cannot be rendered (seeSubagentSpec.problems).duplicate: two skills declare the same twin name; the first wins, the rest are reported.missing: the skill declares a twin and no agent file exists.stale: the agent file was generated from a declaration that has since changed.in_sync: generated and current.handwritten: an agent opsward did not generate, either carrying the name a skill declares for its twin (declared=True), or sharing a non-declaring skill’s name or preloading it viaskills:(declared=False).
- Return type: