Install
$ agentstack add skill-skywatch-bsky-skywatch-agent-skills-authoring-osprey-rules ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Authoring Osprey Rules
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.
Input Expectations
Before starting, you should have received:
- Rule specification — plain-text spec from the planner describing what to build
- Investigator report — project context with labels, models, UDFs, execution graph
- Project paths — rules project directory and osprey-for-atproto repo path
If any of these are missing, report what's missing and stop.
1. Write Models (if needed)
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)- etc.
Rules for model writing:
- EntityJson vs JsonData:
- Use
EntityJsonfor entity identifiers (things that labels attach to) - Use
JsonDatafor primitive values (strings, ints, booleans) - CRITICAL: Never use
JsonDatafor IDs that labels will attach to. UseEntityJsoninstead.
- Import base models:
``sml Import( rules=['models/base.sml'], ) ``
- Naming conventions:
- Variable names: PascalCase for main definitions
- Private/intermediate variables:
_PascalCaseprefix
- Example model extension:
```sml Import( rules=['models/base.sml'], )
_PostText: str = JsonData( path='$.record.text', required=False, )
_PostUrl: Optional[str] = JsonData( path='$.record.facets[].features[].uri', required=False, ) ```
2. Write Rules
Create rule files in the correct directory based on event type.
Directory structure:
- Post rules →
rules/record/post/ - Follow rules →
rules/record/follow/ - Identity rules →
rules/identity/ - Repost rules →
rules/record/repost/ - etc.
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:
- Intermediate variables:
_PascalCaseprefix - Rule names: PascalCase,
Rulesuffix (implicit fromRule()definition) - Descriptive: explain what the rule detects
Rule construction:
Rule(when_all=[...], description=f'...')when_allcontains a list of conditions that must all be true- All conditions must be type
boolorRuleT— do not mix types
3. Wire Effects
Connect 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.
- Before writing an effect, verify the label name in the labels file.
- If the label doesn't exist, tell the user they must add it to
config/labels.yamlfirst. - CRITICAL: Do not hardcode label names not present in the configuration.
- Choose the right effect type:
LabelAdd/LabelRemove→ internal Osprey labels (most common)AtprotoLabel→ emit to Bluesky's Ozone (labels)AtprotoTag→ add/remove Ozone tags (lightweight metadata for tracking/triage; useneg=Trueto remove)DeclareVerdict→ synchronous decision (emit immediately)
- Prevent re-labeling:
- Use
HasAtprotoLabel(entity=UserId, label='label-name')as a guard in the rule'swhen_allto avoid re-labeling. - Pattern:
not _HasLabelX(use negation to skip if already labeled)
- Example with guard:
```sml Import( rules=['models/label_guards.sml'], )
WhenRules( rulesany=[ProfanityRule], then=[ LabelAdd( entity=UserId, label='contains-profanity', expiresafter=Day * 30, ), ], ) ```
4. Wire into Execution Graph
Update the appropriate index.sml to load your new rule file.
Pattern:
- Unconditional:
Require(rule='rules/record/post/new_rule.sml') - Conditional:
Require(rule='...', require_if=IsOperation)
If creating a new event type directory:
- Create the directory:
rules/[event-type]/ - Create its
index.smlwith imports and local requires - Wire the new
index.smlinto the parentrules/index.sml
Example 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:
- [ ] Rule file created in correct directory
- [ ] Rule file imported/required in appropriate
index.sml - [ ] Parent
index.smlupdated if creating new directory - [ ] All imports point to valid model files
5. Report Files Written
After completing all authoring steps, report to the orchestrator:
- Which files were created
- Which files were modified
- What the rule does (brief summary)
Do NOT run validation yourself — the orchestrator dispatches the reviewer for that.
6. Skill Chaining
Load additional skills when you need specialized guidance during authoring.
When to chain to osprey-sml-reference:
- Need SML syntax reference or examples
- Unsure of naming conventions
- Need to look up labeling patterns
- Want to understand how to use list-based matching
Load with: Skill(skill='osprey-sml-reference')
7. Common Mistakes
These are authoring mistakes that cause rules to fail validation or not work as intended.
- Using
JsonDatawhereEntityJsonis required
- Wrong:
UserId: str = JsonData(path='$.did') - Right:
UserId: Entity[str] = EntityJson(type='UserId', path='$.did') - Impact: Labels cannot attach to non-Entity types
- Mixing
RuleTandboolinwhen_alllists
- Wrong:
when_all=[RuleA, SomeBoolean, RuleB] - Right: Keep all conditions as
RuleTor all asbool, don't mix - Impact: Type error, validation fails
- Forgetting to wire new rule into
index.sml
- Wrong: Create
rules/record/post/new_rule.smlbut don'tRequireit - Right: Add
Require(rule='rules/record/post/new_rule.sml')to the appropriate index - Impact: Rule is never executed
- Forgetting to run validation after writing
- Wrong: Assuming the rules work without validation
- Right: The orchestrator dispatches the reviewer after authoring completes
- Impact: Silent failures, invalid rules in production
- Using
rules_all=instead ofrules_any=inWhenRules
- Wrong:
WhenRules(rules_all=[RuleA], then=[...]) - Right:
WhenRules(rules_any=[RuleA], then=[...]) - Impact: Effects don't trigger, validation may fail
- Creating dead rules not referenced by any
WhenRules
- Wrong: Define
Rule(...)but never use it in aWhenRules(...) - Right: Every
Rulemust be referenced by at least oneWhenRules - Impact: Dead code, no effect on labeling
8. Rationalizations to Block
| 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 --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.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: skywatch-bsky
- Source: skywatch-bsky/skywatch-agent-skills
- License: MIT
- Homepage: https://bsky.app/profile/skywatch.blue
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.