[1.1.0] - 2026-10-04
Phase D's first three steps — severe-weather alerts (15), air quality (16) and moon/astro (17) —
on top of the 2026-10-02 review fixes
(docs/reviews/02-review-01-fixes-2026-10-02.md).
The v1 CLI contract is unchanged; the JSON document moves to schema_version 2 because the
alerts array arrived with the alerts work (see below), and the config document stays at
schema_version 1 — the [alerts] and [air] tables are additive.
Added
- Severe-weather alerts (step 15).
cirrocastnow fetches official warnings by default —
--no-alertsopts out,--alertsforces them,--alerts-from nws,meteoalarm,…names the sources —
and renders them as a severity-coloured banner aboveart-table/one-line, asalert:records in
plain, as the new--format alertslisting and as thealertsarray injson. Sources are
selected by coverage: NWS (US and territories), MeteoAlarm (EUMETNET members, optional
CIRROCAST_METEOALARM_KEY), HKO (Hong Kong), QWeather (China, on its provider's chain) and the two
global aggregators WMO SWIC and FPAS (self-hostable through[alerts] fpas_url). Warnings are
normalised to CAP 1.2, expired ones are dropped (ends, elseexpires), duplicates across sources
are collapsed, and strongest comes first. The threshold is[alerts] severity_threshold/--severity
(defaultminor); the cache namespacealerts/has a 300 s TTL and--offlinereplays the last
set. One-line's%Aexpands to the strongest alert's event (empty when none). - JSON output:
schema_versionis now 2, adding thealertsarray andalert_credits
(docs/schema.md); version 1 documents still parse.provider infogained the alert row
(provider info qweather→alerts: qweather,provider info smhi→alerts: none). [alerts]configuration table (enabled,severity_threshold,sources,fpas_url,
cache_ttl_secs) withconfig get/setsupport.- Air quality (step 16).
--aqiappends an air-quality panel toart-tableandplain— the
US and European AQI, the six regulated pollutants in μg/m³ and, inside the CAMS European domain,
the six pollen species in grains/m³ —--format aqiprints it standalone,one-linegains the
%qtoken, andjsoncarries anairobject (additive withinschema_version2). The reading
is one keyless request to Open-Meteo's Air Quality API for the location the run already resolved,
cached underweather/open-meteo-air-<lat>-<lon>-<local-date>.jsonwithcache.weather_ttl_secs
and the usual--no-cache/--refresh/--offlinesemantics. Categories are computed locally from
the published breakpoints;--aqi-index([air] index, defaultus) picks the scale that drives
the category colour and%q;--unitsleaves the pollutant values alone by design. The fetch is
best-effort: a failure printswarning: air quality unavailable: …on stderr and never changes
the run's exit code, and a location outside the pollen domain saysnot covered at this location
instead of inventing a zero. - Moon phase and astronomy (step 17).
--moonappends a locally computed moon/sun block to
art-tableandplain,--format moonprints the standalone view,one-linegains%m(the
phase's art glyph) and%M(the phase's name), andjsoncarries anastroobject (additive
withinschema_version2). Nothing is fetched: the phase, the geocentric illuminated fraction,
the age, moonrise/moonset, the next four phase instants and — when the backend sends no sun
times — sunrise/sunset/daylight are computed from the truncated Meeus series (ELP-2000/82 and
solar, ΔT from the Espenak–Meeus fits). The sun block prefers the provider's own times and records
where they came from inastro.sun.source; inside the polar circles the state is named
(polar day/polar night) instead of clamping to00:00, and an event a day does not have
prints—.%m/%Mare always available;--moonis a usage error for the formats that have
no astro surface (one-line,alerts,aqi).
Changed
network.proxyaccepts onlyhttp://andhttps://URLs. A SOCKS URL is refused by the config
validator, naming the key, instead of reachingureq— which is built without a SOCKS connector and
panicked on a hand-written setting.providers.qweather.hostmust be the account's HTTPS host,https://<account-id>.re.qweatherapi.com.
A legacy shared host or a plain-http://value now fails validation on every run: the legacy hosts
answer403 Invalid Host, and cleartext would leak the key.config showprints the valuesconfig getreports,CIRROCAST_*overrides included, and exits 4
when an override is invalid; it previously ignored the environment.config editrejects unknown keys likeconfig validate;config setvalidates only the key it
writes, so an unrelated invalid value no longer blocks it.config init, andconfig editon a missing file, seed the new file with the effective
configuration when a system document is shadowed (that one case writes canonical TOML without the
commented template; with no other source the template is unchanged).- A whitespace-only
CIRROCAST_*override counts as unset, like a whitespace-only config value. --langaccepts POSIX spellings (zh_CN.UTF-8→zh-CN), anden-*/zh-*tags resolve through
their family chain (en-GB → en-US) without the fallback warning.art-table: thedumb/ASCII arrows are,(SW) and`(NW) — the previous keypad digits read
as part of the speed — and the arrow sector follows the 16-point compass, so arrow and label always
turn together.art-tableat 20–36 columns: the stacked ladder's rungs keep the precipitation and wind fields
instead of silently dropping them (at ≥37 columns the output is unchanged).-f dumbis always plain, escapes included:--color alwaysno longer paints the ASCII table.-f one-line:%wprints the speed alone when the direction is absent, instead ofn/a.-f json:-0.0is written as0.0, like every other display path.-v: the missing-key dump runs after the forecast and reports each missing key once per run;-vv
request logs redact secrets in their percent-encoded spelling too.- Messages: an unknown location no longer promises a candidate list it does not print, an unknown
station points at@lat,lon,--lat/--lonreport the command-line source, and the help epilogue
spells the precedence asLOCATION CIRROCAST_LOCATION.
Fixed
- A reading that is
NaNorinfis refused withError::UpstreaminProvider::fetch— the one
path every backend's answer takes — before it can be cached or rendered. - QWeather: precipitation probability is read as the percent upstream sends (a
40came out as100%
through the fraction helper), and code 515 maps to WMO 56 (freezing drizzle), not a fog variant. - Open-Meteo: an absent or truncated
precipitation_probabilityarray means "no probability", not an
upstream error. - Pirate Weather:
-999sentinels in humidity, cloud cover and probability are missing values, not
readings. - WorldWeatherOnline: the
{"data":{"error":[…]}}envelope is an upstream error carrying the message,
not a decode failure. - METAR: an unmapped obscuration falls back to the sky condition,
ICmaps to WMO 79 (ice pellets,
the nearest described family) instead of failing, and conversions keep the exact value rather than a
pre-rounded one. - A value just below a
.5tie no longer rounds to the wrong side (fmt_inton large readings,
fmt_smallon a negative reading). - An extreme timestamp in an upstream payload produces a typed error instead of overflowing.
- A failed cache write logs at
-vvand still serves the fetched answer. - Nominatim requests are sent once, without retry, so its 1 req/s policy is never breached by backoff.
- A non-absolute
XDG_CONFIG_DIRSentry is ignored instead of read. - A
daysarray whose parts are not exactly[Morning, Noon, Evening, Night]is rejected at
deserialisation, so no renderer can show a part under another part's label. defaults.formatacceptsmoon: the validator's list stopped ataqiand the template comment
atdumb, soconfig set defaults.format moonfailed and a hand-writtenformat = "moon"was
refused on every run although-f moonandCIRROCAST_FORMAT=moonworked. The three lists are
now pinned to each other by a test.