Files
gastown/docs/design/plugin-system.md
gastown/crew/george f5832188a6 docs: add plugin and escalation system designs
Plugin System (gt-n08ix):
- Deacon-dispatched periodic automation
- Dog execution model (non-blocking)
- Wisps for state tracking (no state.json)
- Gate types: cooldown, cron, condition, event
- First plugin: rebuild-gt for stale binary detection

Escalation System (gt-i9r20):
- Unified gt escalate command with severity routing
- Config-driven: settings/escalation.json
- Escalation beads for tracking
- Stale escalation re-escalation
- Actions: bead, mail, email, sms

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:24:58 -08:00

12 KiB

Plugin System Design

Design document for the Gas Town plugin system. Written 2026-01-11, crew/george session.

Problem Statement

Gas Town needs extensible, project-specific automation that runs during Deacon patrol cycles. The immediate use case is rebuilding stale binaries (gt, bd, wv), but the pattern generalizes to any periodic maintenance task.

Current state:

  • Plugin infrastructure exists conceptually (patrol step mentions it)
  • ~/gt/plugins/ directory exists with README
  • No actual plugins in production use
  • No formalized execution model

Design Principles Applied

Discover, Don't Track

Reality is truth. State is derived.

Plugin state (last run, run count, results) lives on the ledger as wisps, not in shadow state files. Gate evaluation queries the ledger directly.

ZFC: Zero Framework Cognition

Agent decides. Go transports.

The Deacon (agent) evaluates gates and decides whether to dispatch. Go code provides transport (gt dog dispatch) but doesn't make decisions.

MEOW Stack Integration

Layer Plugin Analog
Molecule plugin.md - work template with TOML frontmatter
Ephemeral Plugin-run wisps - high-volume, digestible
Observable Plugin runs appear in bd activity feed
Workflow Gate → Dispatch → Execute → Record → Digest

Architecture

Plugin Locations

~/gt/
├── plugins/                      # Town-level plugins (universal)
│   └── README.md
├── gastown/
│   └── plugins/                  # Rig-level plugins
│       └── rebuild-gt/
│           └── plugin.md
├── beads/
│   └── plugins/
│       └── rebuild-bd/
│           └── plugin.md
└── wyvern/
    └── plugins/
        └── rebuild-wv/
            └── plugin.md

Town-level (~/gt/plugins/): Universal plugins that apply everywhere. Rig-level (<rig>/plugins/): Project-specific plugins.

The Deacon scans both locations during patrol.

Execution Model: Dog Dispatch

Key insight: Plugin execution should not block Deacon patrol.

Dogs are reusable workers designed for infrastructure tasks. Plugin execution is dispatched to dogs:

Deacon Patrol                    Dog Worker
─────────────────               ─────────────────
1. Scan plugins
2. Evaluate gates
3. For open gates:
   └─ gt dog dispatch plugin     ──→ 4. Execute plugin
      (non-blocking)                  5. Create result wisp
                                      6. Send DOG_DONE
4. Continue patrol
   ...
5. Process DOG_DONE              ←── (next cycle)

Benefits:

  • Deacon stays responsive
  • Multiple plugins can run concurrently (different dogs)
  • Plugin failures don't stall patrol
  • Consistent with Dogs' purpose (infrastructure work)

State Tracking: Wisps on the Ledger

Each plugin run creates a wisp:

bd wisp create \
  --label type:plugin-run \
  --label plugin:rebuild-gt \
  --label rig:gastown \
  --label result:success \
  --body "Rebuilt gt: abc123 → def456 (5 commits)"

Gate evaluation queries wisps instead of state files:

# Cooldown check: any runs in last hour?
bd list --type=wisp --label=plugin:rebuild-gt --since=1h --limit=1

Derived state (no state.json needed):

Query Command
Last run time bd list --label=plugin:X --limit=1 --json
Run count bd list --label=plugin:X --json | jq length
Last result Parse result: label from latest wisp
Failure rate Count result:failure vs total

Digest Pattern

Like cost digests, plugin wisps accumulate and get squashed daily:

gt plugin digest --yesterday

Creates: Plugin Digest 2026-01-10 bead with summary Deletes: Individual plugin-run wisps from that day

This keeps the ledger clean while preserving audit history.


Plugin Format Specification

File Structure

rebuild-gt/
└── plugin.md      # Definition with TOML frontmatter

plugin.md Format

+++
name = "rebuild-gt"
description = "Rebuild stale gt binary from source"
version = 1

[gate]
type = "cooldown"
duration = "1h"

[tracking]
labels = ["plugin:rebuild-gt", "rig:gastown", "category:maintenance"]
digest = true

[execution]
timeout = "5m"
notify_on_failure = true
+++

# Rebuild gt Binary

Instructions for the dog worker to execute...

TOML Frontmatter Schema

# Required
name = "string"           # Unique plugin identifier
description = "string"    # Human-readable description
version = 1               # Schema version (for future evolution)

[gate]
type = "cooldown|cron|condition|event|manual"
# Type-specific fields:
duration = "1h"           # For cooldown
schedule = "0 9 * * *"    # For cron
check = "gt stale -q"     # For condition (exit 0 = run)
on = "startup"            # For event

[tracking]
labels = ["label:value", ...]  # Labels for execution wisps
digest = true|false            # Include in daily digest

[execution]
timeout = "5m"            # Max execution time
notify_on_failure = true  # Escalate on failure
severity = "low"          # Escalation severity if failed

Gate Types

Type Config Behavior
cooldown duration = "1h" Query wisps, run if none in window
cron schedule = "0 9 * * *" Run on cron schedule
condition check = "cmd" Run check command, run if exit 0
event on = "startup" Run on Deacon startup
manual (no gate section) Never auto-run, dispatch explicitly

Instructions Section

The markdown body after the frontmatter contains agent-executable instructions. The dog worker reads and executes these steps.

Standard sections:

  • Detection: Check if action is needed
  • Action: The actual work
  • Record Result: Create the execution wisp
  • Notification: On success/failure

Escalation System

Problem

Current escalation is ad-hoc "mail Mayor". Issues:

  • Mayor gets backlogged easily
  • No severity differentiation
  • No alternative channels (email, SMS, etc.)
  • No tracking of stale escalations

Solution: Unified Escalation API

New command:

gt escalate \
  --severity=<low|medium|high|critical> \
  --subject="Plugin FAILED: rebuild-gt" \
  --body="Build failed: make returned exit code 2" \
  --source="plugin:rebuild-gt"

Escalation Routing

The command reads town config (~/gt/config.json or similar) for routing rules:

{
  "escalation": {
    "routes": {
      "low": ["bead"],
      "medium": ["bead", "mail:mayor"],
      "high": ["bead", "mail:mayor", "email:human"],
      "critical": ["bead", "mail:mayor", "email:human", "sms:human"]
    },
    "contacts": {
      "human_email": "steve@example.com",
      "human_sms": "+1234567890"
    },
    "stale_threshold": "4h"
  }
}

Escalation Actions

Action Behavior
bead Create escalation bead with severity label
mail:mayor Send mail to mayor/
email:human Send email via configured service
sms:human Send SMS via configured service

Escalation Beads

Every escalation creates a bead:

type: escalation
status: open
labels:
  - severity:high
  - source:plugin:rebuild-gt
  - acknowledged:false

Stale Escalation Patrol

A patrol step (or plugin!) checks for unacknowledged escalations:

bd list --type=escalation --label=acknowledged:false --older-than=4h

Stale escalations get re-escalated at higher severity.

Acknowledging Escalations

gt escalate ack <bead-id>
# Sets label acknowledged:true

New Commands Required

gt stale

Expose binary staleness check:

gt stale              # Human-readable output
gt stale --json       # Machine-readable
gt stale --quiet      # Exit code only (0=stale, 1=fresh)

gt dog dispatch

Formalized plugin dispatch to dogs:

gt dog dispatch --plugin <name> [--rig <rig>]

This:

  1. Finds the plugin definition
  2. Slinga a standardized work unit to an idle dog
  3. Returns immediately (non-blocking)

gt escalate

Unified escalation API:

gt escalate \
  --severity=<level> \
  --subject="..." \
  --body="..." \
  [--source="..."]

gt escalate ack <bead-id>
gt escalate list [--severity=...] [--stale]

gt plugin

Plugin management:

gt plugin list                    # List all plugins
gt plugin show <name>             # Show plugin details
gt plugin run <name> [--force]    # Manual trigger
gt plugin digest [--yesterday]    # Squash wisps to digest
gt plugin history <name>          # Show execution history

Implementation Plan

Phase 1: Foundation

  1. gt stale command - Expose CheckStaleBinary() via CLI
  2. Plugin format spec - Finalize TOML schema
  3. Plugin scanning - Deacon scans town + rig plugin dirs

Phase 2: Execution

  1. gt dog dispatch --plugin - Formalized dog dispatch
  2. Plugin execution in dogs - Dog reads plugin.md, executes
  3. Wisp creation - Record results on ledger

Phase 3: Gates & State

  1. Gate evaluation - Cooldown via wisp query
  2. Other gate types - Cron, condition, event
  3. Plugin digest - Daily squash of plugin wisps

Phase 4: Escalation

  1. gt escalate command - Unified escalation API
  2. Escalation routing - Config-driven multi-channel
  3. Stale escalation patrol - Check unacknowledged

Phase 5: First Plugin

  1. rebuild-gt plugin - The actual gastown plugin
  2. Documentation - So Beads/Wyvern can create theirs

Example: rebuild-gt Plugin

+++
name = "rebuild-gt"
description = "Rebuild stale gt binary from gastown source"
version = 1

[gate]
type = "cooldown"
duration = "1h"

[tracking]
labels = ["plugin:rebuild-gt", "rig:gastown", "category:maintenance"]
digest = true

[execution]
timeout = "5m"
notify_on_failure = true
severity = "medium"
+++

# Rebuild gt Binary

Checks if the gt binary is stale (built from older commit than HEAD) and rebuilds.

## Gate Check

The Deacon evaluates this before dispatch. If gate closed, skip.

## Detection

Check binary staleness:

```bash
gt stale --json

If "stale": false, record success wisp and exit early.

Action

Rebuild from source:

cd ~/gt/gastown/crew/george && make build && make install

Record Result

On success:

bd wisp create \
  --label type:plugin-run \
  --label plugin:rebuild-gt \
  --label rig:gastown \
  --label result:success \
  --body "Rebuilt gt: $OLD$NEW ($N commits)"

On failure:

bd wisp create \
  --label type:plugin-run \
  --label plugin:rebuild-gt \
  --label rig:gastown \
  --label result:failure \
  --body "Build failed: $ERROR"

gt escalate --severity=medium \
  --subject="Plugin FAILED: rebuild-gt" \
  --body="$ERROR" \
  --source="plugin:rebuild-gt"

---

## Open Questions

1. **Plugin discovery in multiple clones**: If gastown has crew/george, crew/max, crew/joe - which clone's plugins/ dir is canonical? Probably: scan all, dedupe by name, prefer rig-root if exists.

2. **Dog assignment**: Should specific plugins prefer specific dogs? Or any idle dog?

3. **Plugin dependencies**: Can plugins depend on other plugins? Probably not in v1.

4. **Plugin disable/enable**: How to temporarily disable a plugin without deleting it? Label on a plugin bead? `enabled = false` in frontmatter?

---

## References

- PRIMING.md - Core design principles
- mol-deacon-patrol.formula.toml - Patrol step plugin-run
- ~/gt/plugins/README.md - Current plugin stub