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:
-
Hard to browse on mobile or while multitasking
-
Inconsistent in API availability (Townsville has ArcGIS open data; South Burnett has mostly web UI)
-
Not hyperlocal by default (one council site serves many towns)
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
-
Assign council + locality per IRC channel (and optionally per bot instance)
-
Provide 5–10 high-value commands that work on Undernet/mIRC-class clients
-
Support capability-based degradation (show only what a council supports)
-
Reuse existing rabbot infrastructure:
RabbotDb,RabbotHttp,IrcFormat,FloodSafe -
Integrate with existing plugins where possible (
QldTraffic,BomWarnings, CKAN patterns fromGovauCkan) -
Ship with tests following
test/plugins/qld_test.rb/test/lib/*_test.rbpatterns -
Document in
lib/COUNCIL_APIS.md(mirrorQLD_APIS.md)
Non-goals (v1)
-
Submitting council forms via IRC
-
Storing user street addresses in SQLite
-
Scraping-heavy features without explicit fallback messaging
-
Full website parity
-
Non-QLD councils (design for extension, implement QLD first)
-
Replacing
!qld,!warn,!bom— bridge/delegate, don’t duplicate long-term
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)
-
Plugin thin; logic in
lib/ -
Use
RabbotHttp.fetch/fetch_json, not rawURI.open -
Use
IrcFormat.report_header/report_footer; plain body text -
Channel state in
RabbotDb.set_plugin_json, not ad-hoc JSON files (except static registry) -
flood_safe_replyfor all IRC output -
# Help:line at top of plugin for!helpdiscovery
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):
-
Explicit command arg (e.g.
!council events wondai) -
Channel plugin_data region
-
BotYAMLregion:field (new optional key inconfig/bots/*.yml) -
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
-
!council set kingaroyresolves vialocality_aliases -
Persist to plugin_data
-
!councilshows council name, locality, capability list -
BotYAMLregion: kingaroyseeds on first message if channel unset (optional hook indispatch_command)
8.2.2 News (!council news)
-
Try council RSS from registry
feeds.news -
If null: try CKAN search for recent datasets/news (best effort)
-
If still empty: link to council news page + last known manual message
-
Parse RSS via pattern from plugins/rss.rb (
REXML, dedupe by GUID/hash) -
Filter headlines by locality keyword when multiple towns share council (e.g. “Wondai” in title)
8.2.3 Contact (!council contact [topic])
-
Serve from registry
contactshash -
Default topic:
customer_service -
Output: label, phone, hours if known, URL
8.2.4 Report (!council report [type])
-
Serve from registry
report_links -
Include 2–3 bullet “what to include” (static text per type in registry)
-
Never POST to council
8.2.5 Search (!council search <query>)
-
CKAN
package_searchon council’s portal (reuseGovauCkanpatterns; extract sharedlib/ckan_search.rbif DRY) -
Append council site search URL as fallback link
-
Townsville: also search ArcGIS Hub API (
/api/search/v1?q=...)
8.2.6 Traffic bridge (!council traffic)
-
Call
QldTraffic.fetch_events+QldTraffic.filter_events(query: locality) -
Format via existing
QldTraffic.format_events -
Title:
Council traffic · Kingaroy
8.2.7 Warnings bridge (!council warnings)
-
Call
BomWarnings.lookup(state: "qld")(or resolve from council) -
Do not duplicate BOM parsing
8.2.8 Status (!council status)
-
Region binding present?
-
HEAD/GET probe: council website, RSS if configured, CKAN portal
-
Report up/down/missing
8.3 Phase 1 acceptance criteria
-
[ ]
!council set kingaroypersists and survives bot restart (SQLite) -
[ ]
!councilin kingaroy shows South Burnett context without args -
[ ]
!council contact wastereturns phone + URL for South Burnett -
[ ]
!council report potholereturns link (even if generic Council Connect) -
[ ]
!council search wastereturns ≥0 CKAN results or clear empty message -
[ ]
!council trafficfilters QLDTraffic to channel locality when set -
[ ] Non-admin cannot
!council set -
[ ] All new tests pass:
ruby -Itest test/plugins/council_test.rb test/lib/council_*_test.rb -
[ ]
!helplists!councilvia# Help:header
8.4 Phase 1 out of scope
-
Timer/watch polling
-
ArcGIS layer queries
-
Bin calendar
-
GTFS parsing
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:
-
Query ArcGIS Hub or Facilities dataset (GeoJSON/WFS from data.gov.au)
-
Return name, suburb, address, accessibility flag if present
-
Filter by query + channel locality
South Burnett:
-
Return static list from registry if maintained (libraries, waste facilities)
-
Else:
!council search facilitiessuggestion + URL
9.2.2 Events (!council events [days])
-
Default days=7, max 30
-
Sources (try in order): events RSS, CKAN event dataset, council /events HTML parse (last resort, fragile)
-
Filter by locality keyword in title/location field
-
Format:
DD Mon — Title — Location(max 5 events)
9.2.3 Bins (!council bins [when])
-
when:today,week(default),next -
If registry has
bins.source: -
static_json: weekly pattern by suburb -
ical_url: parse next collection dates -
csv_url: lookup by suburb column -
If not: clear message:
No live bin calendar for Kingaroy. Check: https://.../waste Tip: ask in channel or check council app.
-
Never guess collection days
9.2.4 Transit (!council transit stop <name>) — Townsville only
-
Download/cache
TSV_GTFS.zip(7-day cache likeQldConfig::GTFS_CACHE_MAX_AGE_SEC) -
Reuse zip parsing patterns from lib/qld_translink_gtfs.rb where possible
-
Stop search only in Phase 2 (no RT departures — TransLink TSV has no GTFS-RT)
-
Message:
Schedules only — no real-time for Townsville buses.
9.3 Caching
CouncilCache.fetch(key:, max_age_sec:, fetch:) # key: "south-burnett-qld/rss/news" # store under data/cache/council/
Default TTLs:
-
RSS: 15 min
-
ArcGIS/CKAN: 1 hour
-
GTFS zip: 7 days
9.4 Phase 2 acceptance criteria
-
[ ]
!council facilities librariesreturns Townsville library locations -
[ ]
!council events 7returns events or explicit “none found” -
[ ]
!council binsfor South Burnett shows unavailable message with URL (not fake data) -
[ ]
!council transit stop cityfinds a Townsville stop from GTFS -
[ ] Capability gating:
!council facilitieson South Burnett degrades gracefully -
[ ] Cache directory used; repeated calls don’t re-fetch within TTL (mock in tests)
-
[ ] Tests cover ArcGIS parse (fixture JSON), events filter, bins unavailable path
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:
-
Per-channel state in plugin_data
-
seenarray capped at 100 IDs -
poll_channels(bot)iteratesbot.config.channels -
Post via
bot.Channel(channel).send(text)
Bin reminder logic
-
At 18:00 local (
Australia/Brisbane), if collection tomorrow for channel locality → post once -
Track
last_bin_reminder_datein watch state to avoid duplicates
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)
-
Max 20 shortcuts per channel
-
Max 200 chars per entry
-
Admin-only add/rem
-
Display in
!council helpwhen present
10.4 Phase 3 acceptance criteria
-
[ ]
!council watch news onenables watch; new RSS item posts to channel (test with fixture + mock timer) -
[ ]
!council watch offdisables all or per-type -
[ ] Traffic watch reuses QLDTraffic filter by channel locality
-
[ ] Bin reminder fires once per collection eve (unit test with frozen time)
-
[ ] Shortcuts add/list/rem work; non-admin blocked
-
[ ] Timer doesn’t crash bot on fetch error (rescue, log, continue)
-
[ ] No duplicate posts for same GUID/event ID
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:
-
Add
counciltopic synonyms -
Add locality hints:
kingaroy,wondai,nanango,south burnett
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
-
[ ]
NewsRSS URL (or confirm none → CKAN/search fallback) -
[ ] Council Connect / report URL
-
[ ] Waste info page
-
[ ] Contact phone numbers
-
[ ] Confirm no public ArcGIS REST endpoint
Townsville City Council
-
[ ] News/notices RSS
-
[ ] ArcGIS Hub search API working
-
[ ] Facilities dataset on data.gov.au
-
[ ] Events source
-
[ ] TSV GTFS zip URL (known:
https://gtfsrt.api.translink.com.au/GTFS/TSV_GTFS.zip)
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
-
Inject
fetch:lambdas (seetest/plugins/qld_test.rb) -
Use
RabbotDb.db_path = temp+reset! -
Test plain-text output includes locality name
-
No live HTTP in unit tests (optional
script/verify_council_apis.rbsmoke script)
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
-
No PII storage: don’t store user addresses for bin lookup
-
HTTPS only for fetch URLs
-
Admin-only config commands
-
Attribution: council name + CC-BY where required (TransLink, TMR, TCC open data)
-
Disclaimer: unofficial bot, verify on council site
-
Rate limiting: respect cache TTLs; max 1 Hub search per command
-
SSRF: only fetch URLs from registry or registered RSS feeds (no arbitrary URL fetch from user input except existing CKAN search query params)
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):
-
data/council_regions.json+lib/council_registry.rb -
lib/council_context.rb -
lib/council_format.rb+ static contacts adapter -
lib/council_adapters/rss.rb+ckan.rb -
Traffic/warnings bridges
-
lib/council_dispatch.rb+plugins/council.rb -
Tests +
lib/COUNCIL_APIS.md -
plugin_catalog.rb + help section
Phase 2:
-
Cache layer
-
ArcGIS adapters + facilities/events/bins/transit
-
Registry enrichment + tests
Phase 3:
-
lib/council_watch.rb+ timer -
Shortcuts
-
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.