PRD: !council — Local Government IRC Gateway for Rabbot

Version: 1.0
Status: Ready for implementation
Target codebase: /home/koda/rabbot
Primary command: !council (alias: !local optional, defer to Phase 3)
Initial councils: South Burnett Regional Council (Kingaroy/Wondai), Townsville City Council
Audience: Cursor agent implementing in 3 phases


1. Executive summary

Build a generic !council plugin that lets small IRC communities access their local government website and open-data services through concise bot commands and passive channel watches.

Each IRC channel can be assigned a locality within an LGA (e.g. kingaroy → South Burnett · Kingaroy). Commands default to that context. The bot acts as a concierge, not a website replacement: short answers, source attribution, links for detail.

Implementation is split into 3 phases:

Phase Goal Outcome
1 Useful everywhere Region assignment, news, contact, report links, search, state feed bridges
2 Structured local data Facilities/hours, events, bin collection where data exists
3 Community automation Watches (news, bins, traffic, warnings), admin shortcuts

2. Problem statement

Small Queensland communities use IRC for day-to-day chat but council information lives on websites that are:

Users repeatedly ask: When is bin day? What’s the library hours? Any council news? How do I report a pothole?

There is no shared “region context” in rabbot today. Location is passed per command (!qld traffic gold coast, !warn townsville). We need channel-level locality assignment and council-aware routing.


3. Goals and non-goals

Goals

Non-goals (v1)


4. Users and personas

Persona Need Example
Community member Quick lookup, no args !council news
Channel op Bind channel to town !council set kingaroy
Bot operator Provision bot for one LGA YAML region: in bot config
Developer Add new council via registry Edit data/council_regions.json

5. Architecture overview

plugins/council.rb              # Cinch plugin, match, timer, execute
lib/council_config.rb           # Constants, paths, plugin name
lib/council_context.rb          # Resolve region: arg > channel > bot > nil
lib/council_registry.rb         # Load/validate data/council_regions.json
lib/council_dispatch.rb         # Route parsed commands to handlers
lib/council_format.rb           # IRC report formatting
lib/council_adapters/
  rss.rb                        # News/notices/events feeds
  ckan.rb                       # data.qld.gov.au / data.gov.au search
  arcgis_hub.rb                 # Hub search API
  arcgis_rest.rb                # Layer query (Townsville)
  static_contacts.rb            # Contacts, report links from registry
  qld_traffic_bridge.rb         # Wrap lib/qld_traffic.rb
  bom_bridge.rb                 # Wrap lib/bom_warnings.rb
lib/council_watch.rb            # Phase 3: poll + post (pattern: qld_traffic_watch.rb)
lib/council_shortcuts.rb        # Phase 3: community-curated entries
data/council_regions.json       # Council + locality definitions
data/cache/council/             # HTTP cache (RSS, ArcGIS, CKAN)
lib/COUNCIL_APIS.md             # Operator docs
test/plugins/council_test.rb
test/lib/council_*_test.rb

Design principles (from lib/CONVENTIONS.md)


6. Region model

6.1 Registry file: data/council_regions.json

Static catalog of supported councils. Schema:

{
  "councils": {
    "south-burnett-qld": {
      "id": "south-burnett-qld",
      "name": "South Burnett Regional Council",
      "state": "QLD",
      "timezone": "Australia/Brisbane",
      "website": "https://www.southburnett.qld.gov.au",
      "localities": ["Kingaroy", "Wondai", "Nanango", "Murgon", "Blackbutt", "Proston"],
      "capabilities": ["news", "contact", "report", "search", "traffic", "warnings"],
      "feeds": {
        "news": null
      },
      "ckan": {
        "portal": "https://www.data.qld.gov.au",
        "search_terms": ["south burnett", "kingaroy"]
      },
      "contacts": {
        "customer_service": { "phone": "4189 9100", "url": "https://www.southburnett.qld.gov.au/Your-Council/Contact-Us" },
        "waste": { "phone": "4189 9100", "url": "https://www.southburnett.qld.gov.au/Live/Waste-and-Recycling" }
      },
      "report_links": {
        "general": { "label": "Council Connect", "url": "https://www.southburnett.qld.gov.au/Home" },
        "pothole": { "label": "Report road issue", "url": "..." },
        "animals": { "label": "Animal complaint", "url": "..." }
      }
    },
    "townsville-qld": {
      "id": "townsville-qld",
      "name": "Townsville City Council",
      "state": "QLD",
      "timezone": "Australia/Brisbane",
      "website": "https://www.townsville.qld.gov.au",
      "localities": ["Townsville", "Aitkenvale", "Thuringowa", "Annandale", "Kirwan"],
      "capabilities": ["news", "contact", "report", "search", "facilities", "events", "traffic", "warnings", "transit_static"],
      "feeds": {
        "news": null
      },
      "arcgis": {
        "hub": "https://data-tsvcitycouncil.opendata.arcgis.com",
        "services_base": "https://maps.townsville.qld.gov.au/arcgis/rest/services"
      },
      "ckan": {
        "portal": "https://data.gov.au",
        "org_filter": "townsville"
      },
      "shortcuts": {
        "libraries": { "type": "arcgis_search", "query": "libraries" },
        "facilities": { "type": "dataset", "name": "townsville-city-council-facilities-and-venues" }
      },
      "contacts": {
        "customer_service": { "phone": "13 48 10", "url": "https://www.townsville.qld.gov.au" }
      },
      "report_links": {
        "general": { "label": "Contact Council", "url": "..." }
      },
      "transit": {
        "gtfs_zip": "https://gtfsrt.api.translink.com.au/GTFS/TSV_GTFS.zip",
        "rt_region": null
      }
    }
  },
  "locality_aliases": {
    "kingaroy": { "council_id": "south-burnett-qld", "locality": "Kingaroy" },
    "wondai": { "council_id": "south-burnett-qld", "locality": "Wondai" },
    "townsville": { "council_id": "townsville-qld", "locality": "Townsville" },
    "tsv": { "council_id": "townsville-qld", "locality": "Townsville" }
  }
}

Agent task: Research and populate real RSS/feed URLs where they exist. Use null + capability omission when unavailable.

6.2 Channel assignment (RabbotDb)

Key Scope Shape
region channel { "council_id": "...", "locality": "Kingaroy", "set_by": "nick", "set_at": "ISO8601" }
watch channel { "news": { "enabled": false, "seen": [] }, "bins": {...}, "traffic": {...}, "warnings": {...} }
shortcuts channel { "entries": [{ "key": "markets", "text": "...", "set_by": "..." }] }

Plugin name constant: CouncilConfig::PLUGIN_NAME = "council".

6.3 Resolution hierarchy

CouncilContext.resolve(network:, channel:, bot_config:, explicit_locality: nil):

  1. Explicit command arg (e.g. !council events wondai)

  2. Channel plugin_data region

  3. Bot YAML region: field (new optional key in config/bots/*.yml)

  4. nil → prompt: No region set. Usage: !council set <town>

Return struct:

CouncilContext::Resolved = Struct.new(
  :council_id, :council, :locality, :timezone, :capabilities, keyword_init: true
)

6.4 Admin gating

!council set, !council watch, !council shortcut add|rem require:

RabbotDb.user_allowed_at_level?(
  network: ..., channel: ..., nick: ..., is_op: ...,
  minimum: RabbotDb::LEVEL_ADMIN
)

Match pattern from plugins/bot_options.rb / plugins/recipe.rb.


7. Command specification

7.1 Command grammar

!council                                          → help / status for channel region
!council help
!council set <locality_or_alias>                  → admin: bind channel
!council clear                                    → admin: remove channel binding
!council regions                                  → list supported councils/localities
!council news [n]                                 → latest headlines (default n=3, max 8)
!council contact [topic]                          → customer_service, waste, etc.
!council report [type]                            → report link + brief instructions
!council search <query>                           → CKAN + optional ArcGIS hub search
!council traffic [locality]                       → delegates to QldTraffic, default locality
!council warnings                                 → delegates to BomWarnings for council state
!council facilities [query]                       → Phase 2, ArcGIS/static
!council events [days]                            → Phase 2, RSS/CKAN
!council bins [when]                              → Phase 2, dataset or honest unavailable
!council transit stop <name>                      → Phase 2, TSV GTFS static only
!council watch on|off [news|bins|traffic|warnings] → Phase 3
!council shortcut list|add|rem                  → Phase 3
!council status                                   → region + backend reachability

Match regex (follow Qld.command_pattern style):

/council(?:\s+(.+))?$/i

Also register in lib/plugin_catalog.rb:

"council" => %w[council local lga government bins waste library report news events facilities]

Add locality hints: kingaroy wondai nanango townsville.

7.2 Response format

All replies use:

[
  IrcFormat.report_header(tag: "COUNCIL", title: "South Burnett · Kingaroy", style: ...),
  # 3-8 plain-text body lines,
  IrcFormat.report_footer("South Burnett Regional Council", optional_url)
].join("\n")

Footer must include data source and link. Prefix disclaimer on first !council in channel (once per day optional):
Unofficial community bot — verify on council website.

7.3 Error messages

Condition Message
No region set No region set for #channel. Ops: !council set kingaroy
Unknown locality Unknown locality. Try: !council regions
Capability missing Not available for South Burnett yet. See: <url>
Fetch failure Council feed unavailable right now. <url>
Permission denied Admin or op required.

Never invent bin days or hours.


8. Phase 1 — Useful everywhere

Objective: Ship core plugin with region assignment and commands that work even for councils with minimal APIs (South Burnett).

8.1 Deliverables

Item Description
plugins/council.rb Plugin shell, parse, dispatch, execute
lib/council_*.rb Context, registry, dispatch, format, adapters (rss, ckan, static, bridges)
data/council_regions.json South Burnett + Townsville entries
lib/COUNCIL_APIS.md Setup, commands, adding councils
Tests Parser, context resolution, news/contact/report/search, permission checks
config/bots/example.yml Comment showing optional region: key

8.2 Features

8.2.1 Region assignment

8.2.2 News (!council news)

8.2.3 Contact (!council contact [topic])

8.2.4 Report (!council report [type])

8.2.5 Search (!council search <query>)

8.2.6 Traffic bridge (!council traffic)

8.2.7 Warnings bridge (!council warnings)

8.2.8 Status (!council status)

8.3 Phase 1 acceptance criteria

8.4 Phase 1 out of scope


9. Phase 2 — Structured local data

Objective: Add facilities, events, bins, and static transit where structured data exists. Townsville-first; South Burnett gets honest fallbacks.

9.1 Deliverables

Item Description
lib/council_adapters/arcgis_hub.rb Hub search
lib/council_adapters/arcgis_rest.rb Feature layer query
lib/council_adapters/events.rb RSS iCal/JSON/events page
lib/council_adapters/bins.rb CSV/JSON/iCal or static schedule
lib/council_adapters/transit_static.rb GTFS zip parse (minimal: stop search only)
Cache layer data/cache/council/<council_id>/ with TTL per adapter
Registry updates Townsville shortcuts; South Burnett bins: unavailable note

9.2 Features

9.2.1 Facilities (!council facilities [query])

Townsville:

South Burnett:

9.2.2 Events (!council events [days])

9.2.3 Bins (!council bins [when])

No live bin calendar for Kingaroy. Check: https://.../waste Tip: ask in channel or check council app.

9.2.4 Transit (!council transit stop <name>) — Townsville only

9.3 Caching

CouncilCache.fetch(key:, max_age_sec:, fetch:)
# key: "south-burnett-qld/rss/news"
# store under data/cache/council/

Default TTLs:

9.4 Phase 2 acceptance criteria


10. Phase 3 — Community automation

Objective: Passive watches and community shortcuts. Make the bot feel “always on” for small towns.

10.1 Deliverables

Item Description
lib/council_watch.rb Poll loop, seen-ID dedupe
lib/council_shortcuts.rb CRUD for channel shortcuts
Timer in plugins/council.rb POLL_INTERVAL_SEC = 900 (15 min, match Rss)
!council watch commands on/off per watch type

10.2 Watch types

Watch Poll source Post when Default
news Council RSS New item GUID not in seen off
traffic QLDTraffic past-hour New event matching locality + types off
warnings BOM QLD warnings New/changed warning affecting region off
bins Bin adapter Evening before collection day (local TZ) off

Follow QldTrafficWatch pattern:

Bin reminder logic

10.3 Shortcuts

!council shortcut list
!council shortcut add markets Wondai markets Sat 7am Coronation Park
!council shortcut rem markets
!council markets          → shortcut alias (optional sugar)

10.4 Phase 3 acceptance criteria


11. Integration with existing rabbot

11.1 Reuse (do not reimplement)

Module Use
lib/qld_traffic.rb Traffic fetch/filter/format
lib/bom_warnings.rb Warnings
lib/govau_ckan.rb CKAN search (extract shared helper if needed)
lib/qld_translink_gtfs.rb GTFS zip parsing reference
lib/rabbot_http.rb All HTTP
lib/irc_format.rb All formatting
lib/flood_safe.rb Replies

11.2 Plugin catalog / room reader

Update lib/plugin_catalog.rb:

Room reader can then suggest !council for “council bins kingaroy” ideas.

11.3 Help

Add to lib/help_sections.rb under appropriate category (e.g. geo/maps or new “local” group):

plugins: %w[... council]

11.4 Optional bot YAML

Document in config/bots/example.yml:

# Optional default region for all channels on this bot (locality alias from council_regions.json)
# region: townsville

Parse in council context if present (bot config accessible from plugin via bot.config — agent to verify how other plugins read custom YAML keys; may need loader change).


12. Data sourcing checklist for agent

Agent must verify URLs at implementation time:

South Burnett Regional Council

Townsville City Council

Add findings to lib/COUNCIL_APIS.md.


13. Testing strategy

13.1 Test files

test/plugins/council_test.rb       # dispatch, execute, permissions
test/lib/council_context_test.rb   # resolution hierarchy
test/lib/council_registry_test.rb  # JSON load, alias lookup
test/lib/council_rss_test.rb       # RSS parse fixtures
test/lib/council_watch_test.rb      # Phase 3 dedupe, enable/disable
test/fixtures/council/              # RSS XML, ArcGIS JSON, CKAN JSON

13.2 Patterns

13.3 Verify script (optional)

ruby script/verify_council_apis.rb
# Prints reachability for each council in registry

Mirror script/verify_qld_apis.rb.


14. Security and compliance


15. Rollout plan

Step Action
1 Implement Phase 1 behind !council
2 Deploy to dev bot (config/bots/example.yml)
3 Bind test channel: !council set kingaroy
4 Run verify script + manual IRC smoke
5 Phase 2 Townsville facilities/events
6 Phase 3 watches on townsville / kingaroy trial
7 Document adding new councils in COUNCIL_APIS.md

16. Success metrics

Metric Target
Phase 1 command success rate 95% return useful answer or clear fallback
Response size ≤512 chars typical, flood_safe for longer
Watch false duplicate rate 0 duplicates per GUID/24h
Test coverage All dispatch paths have unit tests
New council onboarding Add entry to JSON + docs, no plugin code change for contact/report/search

17. Agent implementation order

Phase 1 (do first):

  1. data/council_regions.json + lib/council_registry.rb

  2. lib/council_context.rb

  3. lib/council_format.rb + static contacts adapter

  4. lib/council_adapters/rss.rb + ckan.rb

  5. Traffic/warnings bridges

  6. lib/council_dispatch.rb + plugins/council.rb

  7. Tests + lib/COUNCIL_APIS.md

  8. plugin_catalog.rb + help section

Phase 2:

  1. Cache layer

  2. ArcGIS adapters + facilities/events/bins/transit

  3. Registry enrichment + tests

Phase 3:

  1. lib/council_watch.rb + timer

  2. Shortcuts

  3. Watch tests + verify script


18. Example IRC sessions (acceptance demos)

<alice> !council set kingaroy
<Rabbot> Council region set: South Burnett · Kingaroy (admin: alice)

<bob> !council
<Rabbot> [COUNCIL] South Burnett · Kingaroy
         Available: news, contact, report, search, traffic, warnings
         !council help for commands
         (South Burnett Regional Council)

<bob> !council news
<Rabbot> [COUNCIL] News · Kingaroy
         12 Jun — Council expands feral pig control measures
         12 Jun — Winter rabbit baiting program open
         More: https://www.southburnett.qld.gov.au/Home

<bob> !council report pothole
<Rabbot> [COUNCIL] Report · pothole
         Use Council Connect with location details and a photo if possible.
         https://...

<carol> !council bins
<Rabbot> [COUNCIL] Bin collection · Kingaroy
         No live bin calendar available via API.
         Check: https://www.southburnett.qld.gov.au/Live/Waste-and-Recycling

19. Open questions for operator (defaults assumed)

Question PRD default
Command name !council vs !local !council primary; alias optional later
Non-QLD expansion Out of scope v1
Scrape council /events HTML Phase 2 last resort only
AI summarisation of news No — titles only

End of PRD. Paste this entire document into a new Cursor agent session with: “Implement Phase 1 of this PRD in rabbot.” Then Phase 2, then Phase 3 as follow-up tasks.