# Skywatch Investigating Osprey Rules

> Systematic investigation methodology for Osprey SML rules projects. Produces structured text reports on project structure, labels, models, UDFs, and execution graphs.

- **Type:** Skill
- **Install:** `agentstack add skill-skywatch-bsky-skywatch-agent-skills-skywatch-investigating-osprey-rules`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [skywatch-bsky](https://agentstack.voostack.com/s/skywatch-bsky)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [skywatch-bsky](https://github.com/skywatch-bsky)
- **Source:** https://github.com/skywatch-bsky/skywatch-agent-skills/tree/main/polytoken/skills/skywatch-investigating-osprey-rules
- **Website:** https://bsky.app/profile/skywatch.blue

## Install

```sh
agentstack add skill-skywatch-bsky-skywatch-agent-skills-skywatch-investigating-osprey-rules
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Investigating Osprey Rules

## Overview

You are investigating an Osprey SML rules project to produce a structured report.
Your caller has provided a rules project path and an osprey-for-atproto repo path.

**Output rules:**
- Return ALL findings as text in your response. NEVER write files.
- Include exact file paths and line numbers for every finding.
- If something is missing or unexpected, report it explicitly — do not silently skip.
- Structure your report with the section headings defined below.

**Investigation order:**
1. Project Structure Inventory (this section)
2. UDF Discovery (Section 2)
3. Execution Graph Mapping (Section 3)

Produce each section in order. If a section cannot be completed (e.g., path
inaccessible), report what's missing and continue to the next section.

---

## Section 1: Project Structure Inventory

Validate the project directory structure, catalogue all SML files, extract the
labels table from `config/labels.yaml`, and list model files with their key
variable definitions.

### Step 1.1: Validate Required Structure

Use `glob` and `file_read` to check for the required project components. Report each as
present or absent.

**Required components (check in this order):**

| Component | Check | Required |
|-----------|-------|----------|
| `main.sml` | File exists at project root | Yes |
| `config/` | Directory exists | Yes |
| `config/labels.yaml` | File exists | Yes |
| `models/` | Directory exists | Yes |
| `rules/` | Directory exists | Yes |
| `rules/index.sml` | File exists | Expected but not fatal if absent |

**Report format:**

```
## 1. Project Structure Inventory

### 1.1 Structure Validation

Project path: /path/to/rules-project

  ✓ main.sml
  ✓ config/
  ✓ config/labels.yaml
  ✓ models/
  ✓ rules/
  ✓ rules/index.sml
```

**If ANY required component is missing:**

```
  ✗ models/ — MISSING (required)
  ✓ rules/
```

Report ALL missing components — do not stop at the first failure. Continue
investigation with whatever IS present.

### Step 1.2: List All SML Files

Use `glob` to find all `.sml` files in the project.

```
`glob` pattern: **/*.sml
```

**Report format:**

```
### 1.2 SML File Inventory

Found N .sml files:

  main.sml
  models/base.sml
  models/record/post.sml
  models/record/follow.sml
  models/label_guards.sml
  rules/index.sml
  rules/record/index.sml
  rules/record/post/index.sml
  rules/record/post/spam_detection.sml
  ...
```

List paths relative to the project root. Sort alphabetically.

### Step 1.3: Extract Labels Table

Read `config/labels.yaml` and produce a table of all defined labels.

**What to extract for each label:**
- Label name (the YAML key)
- `valid_for` values (entity types: UserId, AtUri, PdsHost, Handle, etc.)
- `connotation` value (neutral, positive, or negative)

**Report format:**

```
### 1.3 Labels

Source: config/labels.yaml

  Label Name              | Valid For              | Connotation
  ----------------------- | ====================== | -----------
  alt-gov                 | UserId                 | neutral
  alt-tech                | AtUri                  | neutral
  amplifier               | UserId                 | neutral
  spam-post               | UserId, AtUri          | negative
  ...

Total: N labels defined
```

If `config/labels.yaml` does not exist, report:

```
### 1.3 Labels

  ✗ config/labels.yaml not found — cannot extract labels table
```

### Step 1.4: Catalogue Model Files

Read each file in `models/` (and subdirectories). For each file, extract the key
variable definitions.

**Variable types to identify:**

| Variable Pattern | Type | Purpose |
|-----------------|------|---------|
| `EntityJson(type='X', path='...')` | Entity definition | Label target — used in LabelAdd/LabelRemove entity= |
| `JsonData(path='...')` | Primitive extraction | Data values used in rule conditions |
| `Second = 1`, `Minute = 60`, etc. | Time constant | Duration values for windows and deltas |
| `HasAtprotoLabel(entity=X, label='Y')` | Label guard | Pre-computed check preventing re-labeling |

**For each model file, report:**

```
### 1.4 Models

#### models/base.sml

  Line  | Variable            | Type           | Details
  ----- | =================== | ============== | -------
  3     | UserId              | EntityJson     | type='UserId', path='$.did'
  5     | Handle              | EntityJson     | type='Handle', path='$.handle'
  8     | ActionName          | JsonData       | path='$.action', type=str
  12    | Second              | Time constant  | = 1
  13    | Minute              | Time constant  | = 60
  14    | Hour                | Time constant  | = 3600
  15    | Day                 | Time constant  | = 86400
  16    | Week                | Time constant  | = 604800

#### models/record/post.sml

  Line  | Variable            | Type           | Details
  ----- | =================== | ============== | -------
  5     | PostText            | JsonData       | path='$.record.text', type=str
  7     | PostUri             | EntityJson     | type='AtUri', path='$.uri'
  ...

#### models/label_guards.sml

  Line  | Variable            | Type           | Details
  ----- | =================== | ============== | -------
  4     | HasSpamLabel        | Label guard    | entity=UserId, label='spam'
  5     | HasBotLabel         | Label guard    | entity=UserId, label='bot'
  ...
```

Include line numbers for every variable definition. If `models/` does not exist,
report it as missing and skip this step.

---

## Section 2: UDF Discovery

Discover all available User-Defined Functions (UDFs) that can be called from SML
rules. Use **dynamic discovery** (reading Python source) when the osprey-for-atproto
repo is accessible, falling back to **static discovery** (reading the reference file)
when it is not.

### Step 2.1: Attempt Dynamic Discovery

Try to locate `register_plugins.py` in the osprey-for-atproto repo path provided
by the caller.

**Expected location:** `{osprey-for-atproto-path}/plugins_atproto/src/register_plugins.py`

Use `file_read` to check if this file exists.

**If the file exists:** Proceed with dynamic discovery (Steps 2.2-2.4).

**If the file does NOT exist:** Skip to Step 2.5 (Static Fallback). Report:

```
### 2.1 UDF Discovery Mode

  ✗ register_plugins.py not found at expected location
  → Falling back to static reference (may be outdated)
  Discovery mode: STATIC (low confidence)
```

### Step 2.2: Extract UDF Class Names

Read `register_plugins.py` and find the `register_udfs()` function. Extract every
class name returned in the list.

**What to look for:**
- A function decorated with `@hookimpl_osprey` named `register_udfs`
- It returns a list of class references (e.g., `TextContains`, `IncrementWindow`)
- The imports at the top of the file show where each class is imported from

**Report format:**

```
### 2.1 UDF Discovery Mode

  ✓ register_plugins.py found
  Discovery mode: DYNAMIC (high confidence)

### 2.2 Registered UDFs

  Found N UDFs registered in register_udfs():

  Standard:
    TextContains          ← udfs/std/text.py
    Tokenize              ← udfs/std/tokenize.py
    CleanString           ← udfs/std/censorize.py
    ...

  Cache:
    CacheGetStr           ← udfs/std/cache.py
    IncrementWindow       ← udfs/std/cache.py
    ...

  AT Protocol Query:
    DidFromUri            ← udfs/atproto/std/did_from_uri.py
    GetRecordURI          ← udfs/atproto/atproto.py
    ...

  AT Protocol Effects:
    AddAtprotoLabel       ← udfs/atproto/atproto_label.py
    RemoveAtprotoLabel    ← udfs/atproto/atproto_label.py
    ...
```

Group UDFs by the comment sections in `register_udfs()` (e.g., `# Std`,
`# Atproto std`, `# Atproto effects`).

### Step 2.3: Extract UDF Signatures

For each UDF class found in Step 2.2, locate its source file (from the import
statements) and extract:

1. **Arguments class:** Find the class referenced as the first type parameter of
   `UDFBase[ArgumentsClass, ReturnType]`. Read its fields to get parameter names,
   types, and defaults.

2. **Return type:** The second type parameter of `UDFBase[ArgumentsClass, ReturnType]`.

**How to identify arguments:**
- Look for `class ClassName(UDFBase[SomeArgs, ReturnType]):`
- Then find `class SomeArgs(ArgumentsBase):` (or a subclass of ArgumentsBase)
- Each field annotation is a parameter: `name: type` or `name: type = default`
- Some argument classes inherit from intermediate classes (e.g.,
  `CacheWindowArgumentsBase` extends `CacheArgumentsBase` which extends
  `ArgumentsBase`) — follow the inheritance chain to get all fields

**Report format:**

```
### 2.3 UDF Signatures

#### TextContains
  Source: udfs/std/text.py:47
  Arguments: TextContainsArguments (udfs/std/text.py:41)
    s: str                        (required)
    phrase: str                   (required)
    case_sensitive: bool          (default: False)
  Returns: bool

#### IncrementWindow
  Source: udfs/std/cache.py:364
  Arguments: IncrementWindowArguments (udfs/std/cache.py:279)
    Inherits from CacheWindowArgumentsBase → CacheArgumentsBase → ArgumentsBase
    key: str                      (required, from CacheArgumentsBase)
    window_seconds: float         (required, from CacheWindowArgumentsBase)
    when_all: List[bool]          (required, from CacheWindowArgumentsBase)
    max_ttl_seconds: Optional[float]  (default: None)
  Returns: int
```

Include line numbers for every class definition.

### Step 2.4: Produce Signature Summary Table

After extracting all signatures, produce a summary table for quick reference:

```
### 2.4 UDF Signature Summary

| UDF | Parameters | Returns |
|-----|-----------|---------|
| TextContains | s: str, phrase: str, case_sensitive=False | bool |
| ForceString | s: Optional[str] | str |
| ExtractDomains | s: str | List[str] |
| ... | ... | ... |

Total: N UDFs with signatures extracted
Discovery: DYNAMIC from {osprey-for-atproto-path}
```

### Step 2.5: Static Fallback

**Use this step ONLY when dynamic discovery failed (Step 2.1).**

Read the static reference file at:
`{skill-directory}/references/udf-signatures.md`

(The skill directory is the directory containing this SKILL.md file.)

**Report format:**

```
### 2.1 UDF Discovery Mode

  ✗ Dynamic discovery unavailable
  → Using static reference: references/udf-signatures.md
  Discovery mode: STATIC (low confidence — signatures may be outdated)

### 2.2-2.4 UDF Signatures (from static reference)

[Include the full content of references/udf-signatures.md]
```

Always include the staleness caveat when using static fallback.

---

## Section 3: Execution Graph Mapping

Trace the full execution graph from `main.sml` through all `Import` and `Require`
chains. Catalogue every `Rule()` definition and every `WhenRules()` invocation to
produce a complete map of what the rules project detects and what actions it takes.

### Step 3.1: Trace from main.sml

Read `main.sml` at the project root. Extract:

1. **Import statements:** `Import(rules=['path/to/file.sml'])` — these load models
   and definitions into scope
2. **Require statements:** `Require(rule='path/to/file.sml')` — these load rule
   files into the execution graph
3. **Conditional requires:** `Require(rule='path/to/file.sml', require_if=Condition)`
   — note the condition that gates loading

**Then follow each Require chain recursively.** For each required file:
- Read it
- Extract its Import and Require statements
- Follow those Requires in turn
- Continue until you reach leaf files (files with no further Requires)

**Report format:**

```
## 3. Execution Graph

### 3.1 Execution Graph Trace

main.sml
  ├── Import: models/base.sml
  └── Require: rules/index.sml

rules/index.sml
  ├── Import: models/base.sml
  ├── Require: rules/record/index.sml (require_if=IsOperation)
  └── Require: rules/identity/index.sml (require_if=ActionName=='identity')

rules/record/index.sml
  ├── Import: models/base.sml, models/record/post.sml
  ├── Require: rules/record/post/index.sml
  └── Require: rules/record/follow/index.sml

rules/record/post/index.sml
  ├── Import: models/base.sml, models/record/post.sml
  ├── Require: rules/record/post/spam_detection.sml
  └── Require: rules/record/post/profanity.sml

rules/record/post/spam_detection.sml  [LEAF]
  └── Import: models/base.sml, models/record/post.sml, models/label_guards.sml

rules/record/post/profanity.sml  [LEAF]
  └── Import: models/base.sml, models/record/post.sml
```

Mark leaf files (those with no `Require` statements) with `[LEAF]`.
Include the `require_if=` condition for conditional requires.

### Step 3.2: Catalogue Rule() Definitions

For each leaf file (and any file containing `Rule()` definitions), extract every
`Rule()` definition.

**What to extract:**
- Variable name the Rule is assigned to (e.g., `SpamContentRule`)
- Whether the variable has a `_` prefix (file-local, cannot be imported)
- The `when_all` conditions (list each condition on its own line)
- The `description` string if present

**Report format:**

```
### 3.2 Rule Definitions

#### rules/record/post/spam_detection.sml

  Line | Rule Variable          | Scope  | Conditions (when_all)
  ---- | ====================== | ====== | =====================
  12   | _IsNewAccount          | local  | AccountAgeSeconds < Day
       |                        |        | FollowersCount < 5
  18   | _HasSuspiciousContent  | local  | ListContains(list='spam_keywords', phrases=[PostTextCleaned]) != None
  24   | SpamDetection          | export | _IsNewAccount
       |                        |        | _HasSuspiciousContent
       |                        |        | not HasLabel(entity=UserId, label='verified')

#### rules/record/post/profanity.sml

  Line | Rule Variable          | Scope  | Conditions (when_all)
  ---- | ====================== | ====== | =====================
  10   | ProfanityDetected      | export | RegexMatch(pattern=r'...', target=PostText, case_insensitive=True)

Total: N rules across M files
```

**Scope column:**
- `local` = variable starts with `_` (file-private)
- `export` = no `_` prefix (importable by other files)

### Step 3.3: Catalogue WhenRules() Invocations

For each file containing `WhenRules()`, extract:

1. **Which rules trigger it:** The `rules_any=` list
2. **What effects fire:** The `then=` list, including:
   - Effect type (`LabelAdd`, `LabelRemove`, `AtprotoLabel`, `DeclareVerdict`, etc.)
   - Target entity (e.g., `UserId`, `AtUri`, `PdsHost`)
   - Label name or action
   - Any conditional (`apply_if=`) or temporal (`expires_after=`) modifiers

**Report format:**

```
### 3.3 WhenRules Invocations

#### rules/record/post/spam_detection.sml

  WhenRules at line 30:
    Triggers (rules_any):
      - SpamDetection
    Effects (then):
      - LabelAdd(entity=UserId, label='spam_suspect')
      - LabelAdd(entity=AtUri, label='spam_content')

  WhenRules at line 38:
    Triggers (rules_any):
      - SpamDetection
    Effects (then):
      - AtprotoLabel(entity=UserId, label='spam', comment='Automated spam detection')

#### rules/record/post/profanity.sml

  WhenRules at line 16:
    Triggers (rules_any):
      - ProfanityDetected
    Effects (then):
      - LabelAdd(entity=UserId, label='profanity', expires_after=TimeDelta(days=7))

Total: N WhenRules invocations across M files
```

### Step 3.4: Rule → Conditions → Effect → Label Summary

Produce a flat summary table linking every rule to its final outcomes. This is the
most actionable output for rule-writing agents — it shows at a glance what each
rule does.

**Report format:**

```
### 3.4 Rule Summary

| Rule | File | Conditions | Effect | Label | Entity |
|------|------|-----------|--------|-------|--------|
| SpamDetection | spam_detection.sml:24 | _IsNewAccount AND _HasSuspiciousContent AND NOT verified | LabelAdd | spam_suspect | UserId |
| SpamDetection | spam_detection.sml:24 | (same) | LabelAdd | spam_content | AtUri |
| SpamDetection | spam_detection.sml:24 | (same) | AtprotoLabel | spam | UserId |
| ProfanityDetected

…

## 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](https://github.com/skywatch-bsky)
- **Source:** [skywatch-bsky/skywatch-agent-skills](https://github.com/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.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-skywatch-bsky-skywatch-agent-skills-skywatch-investigating-osprey-rules
- Seller: https://agentstack.voostack.com/s/skywatch-bsky
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
