From osprey-rules
Use when writing or modifying Osprey SML rule files from a validated rule specification. Covers model writing, rule writing, effect wiring, and execution graph wiring. Not triggered on general coding tasks.
How this skill is triggered — by the user, by Claude, or both
Slash command
/osprey-rules:authoring-osprey-rulesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This workflow guides you through writing valid Osprey SML from a rule specification.
This workflow guides you through writing valid Osprey SML from a rule specification. You receive a confirmed rule spec from the planner and project context from the investigator. Your job is to write the SML files.
Before starting, you should have received:
If any of these are missing, report what's missing and stop.
If the rule needs features not already defined in existing models, create or extend a model file.
Model hierarchy:
models/base.sml → global definitions (UserId, Handle, ActionName, time constants)models/record/base.sml → features available on all record typesmodels/record/post.sml → post-specific features (text, URLs, mentions)Rules for model writing:
EntityJson vs JsonData:
EntityJson for entity identifiers (things that labels attach to)JsonData for primitive values (strings, ints, booleans)JsonData for IDs that labels will attach to. Use EntityJson instead.Import base models:
Import(
rules=['models/base.sml'],
)
Naming conventions:
_PascalCase prefixExample model extension:
Import(
rules=['models/base.sml'],
)
_PostText: str = JsonData(
path='$.record.text',
required=False,
)
_PostUrl: Optional[str] = JsonData(
path='$.record.facets[*].features[*].uri',
required=False,
)
Create rule files in the correct directory based on event type.
Directory structure:
rules/record/post/rules/record/follow/rules/identity/rules/record/repost/Rule file pattern:
Import(
rules=[
'models/base.sml',
'models/record/post.sml', # or appropriate model file
],
)
_IsProfanity = ContainsAnyPattern(
text=PostText,
patterns=ProfanityList,
)
Rule(
when_all=[
_IsProfanity,
UserId != None,
],
description=f'Post contains profanity',
)
Naming conventions:
_PascalCase prefixRule suffix (implicit from Rule() definition)Rule construction:
Rule(when_all=[...], description=f'...')when_all contains a list of conditions that must all be truebool or RuleT — do not mix typesConnect rules to effects via WhenRules().
Pattern:
WhenRules(
rules_any=[RuleName],
then=[
LabelAdd(entity=UserId, label='label-name'),
],
)
Critical constraints:
Only use labels that exist in config/labels.yaml.
config/labels.yaml first.Choose the right effect type:
LabelAdd / LabelRemove → internal Osprey labels (most common)AtprotoLabel → emit to Bluesky's OzoneDeclareVerdict → synchronous decision (emit immediately)Prevent re-labeling:
HasAtprotoLabel(entity=UserId, label='label-name') as a guard in the rule's when_all to avoid re-labeling.not _HasLabelX (use negation to skip if already labeled)Example with guard:
Import(
rules=['models/label_guards.sml'],
)
WhenRules(
rules_any=[ProfanityRule],
then=[
LabelAdd(
entity=UserId,
label='contains-profanity',
expires_after=Day * 30,
),
],
)
Update the appropriate index.sml to load your new rule file.
Pattern:
Require(rule='rules/record/post/new_rule.sml')Require(rule='...', require_if=IsOperation)If creating a new event type directory:
rules/[event-type]/index.sml with imports and local requiresindex.sml into the parent rules/index.smlExample wiring:
# rules/record/post/index.sml
Import(
rules=['models/base.sml'],
)
Require(rule='rules/record/post/profanity_rule.sml')
Require(rule='rules/record/post/spam_rule.sml')
Then update rules/record/index.sml:
Require(rule='rules/record/post/index.sml')
Verification checklist:
index.smlindex.sml updated if creating new directoryAfter completing all authoring steps, report to the orchestrator:
Do NOT run validation yourself — the orchestrator dispatches the reviewer for that.
Load additional skills when you need specialized guidance during authoring.
When to chain to osprey-sml-reference:
Load with: Skill(skill='osprey-sml-reference')
These are authoring mistakes that cause rules to fail validation or not work as intended.
Using JsonData where EntityJson is required
UserId: str = JsonData(path='$.did')UserId: Entity[str] = EntityJson(type='UserId', path='$.did')Mixing RuleT and bool in when_all lists
when_all=[RuleA, SomeBoolean, RuleB]RuleT or all as bool, don't mixForgetting to wire new rule into index.sml
rules/record/post/new_rule.sml but don't Require itRequire(rule='rules/record/post/new_rule.sml') to the appropriate indexForgetting to run validation after writing
Using rules_all= instead of rules_any= in WhenRules
WhenRules(rules_all=[RuleA], then=[...])WhenRules(rules_any=[RuleA], then=[...])Creating dead rules not referenced by any WhenRules
Rule(...) but never use it in a WhenRules(...)Rule must be referenced by at least one WhenRules| Rationalization | Reality | Action |
|---|---|---|
| "I'll validate later" | No. The orchestrator runs validation immediately after authoring. Do not skip steps hoping validation will catch them. | Write correct SML the first time. Follow the skill steps in order. |
| "I'll skip the index wiring" | No. Rules not in the execution graph don't run. | Update index.sml to require the new rule. Verify the wiring is correct. |
| "I don't need to check labels.yaml" | No. Using undefined labels is a validation error. | Every effect must reference a label that exists in config/labels.yaml. |
| "The model file is correct, I'll ship it" | No. Models are compile-time dependencies. | Double-check EntityJson vs JsonData usage. Verify imports are correct. |
| "I'll use JsonData for this entity ID" | No. Entity IDs must be EntityJson. | Use EntityJson for anything that will be labeled. Use JsonData only for primitive values. |
| "osprey-cli will catch it" | Validation catches syntax errors, not all logic or convention violations. | Follow the authoring steps carefully. Don't rely on validation as your only safety net. |
| "86400 is clearer than Day" | It's not. Time constants from models/base.sml are the convention. | Replace all hardcoded time values: 86400 → Day, 3600 → Hour, 604800 → Week, etc. |
| "I'll just run osprey-cli directly" | It's not on PATH. It must be invoked via uv run from the osprey-for-atproto repo. | Always use uv run osprey-cli push-rules <path> --dry-run from the osprey repo. |
| "This is urgent, skip validation" | Urgency doesn't excuse broken rules. The orchestrator validates after you're done — your job is to write correct SML. | Follow every step. Correct SML is faster than debugging broken SML. |
Output: SML files written to the rules project. Report which files were created or modified back to the orchestrator.
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub skywatch-bsky/claude-skills --plugin osprey-rules