Problem Statement
A machine running red-dev knows things about itself that its owner has to go and ask for. How many AFK Workers are grinding right now lives behind redskilled statusline in a terminal. Which address the machine answers on lives behind ip addr on one side of the WSL boundary and Get-NetIPAddress on the other, and the two disagree.
Both are glanceable facts — the kind you want to read across the room, or on a locked screen, without unlocking anything or opening a terminal. Today the desktop shows brand art and nothing else, and the state is a command away at exactly the moment you cannot type one.
Solution
Redwall: the theme's wallpaper with live machine state drawn over it.
The brand art underneath is unchanged — Redwall composes on top of it, never in place of it. It reaches two surfaces from one image: the desktop background, and the GNOME lock screen where the shell honours it. What it carries is the count of active Workers and the address this machine answers on.
It is off by default and turned on the way a theme or a font is: asked once at first run, recorded in preferences, changeable later from the menu. It regenerates when the state it displays changes, driven by a hook the RedSkills daemon fires — not by a clock.
User Stories
- As a developer with AFK Workers running, I want the count of active Workers on my wallpaper, so that I can see whether the queue is draining without switching to a terminal.
- As a developer away from my desk, I want the Worker count on the lock screen, so that I can check progress in passing without unlocking.
- As a developer on a machine I reach over SSH, I want its LAN address on the wallpaper, so that I can read the address off the screen instead of hunting for it.
- As a WSL user, I want the address that actually reaches this machine, so that I am not shown the
172.x NAT address that no other host can route to.
- As a developer whose laptop moves between Wi-Fi and Ethernet, I want the address to follow the default route, so that it stays correct without me maintaining an interface list.
- As a developer on a VPN, I want to pin a specific interface, so that Redwall shows the address I actually want people to use.
- As a developer who has not started the daemon, I want Redwall to still show my address, so that half the information is not withheld because the other half is unavailable.
- As someone who values their desktop, I want Redwall off until I ask for it, so that installing a provisioning tool does not silently redraw my background.
- As a new user going through first-run setup, I want to be asked about Redwall alongside theme and font, so that I learn it exists at the moment I am choosing how things look.
- As an existing user, I want to enable Redwall from
red-dev menu, so that I do not have to reinstall to change my mind.
- As a user who switches themes, I want Redwall to follow onto the new theme's art, so that enabling it does not pin me to one wallpaper.
- As a user of the light themes, I want the text legible over pale art, so that Redwall is readable on Marble and Flare and not only on Obsidian.
- As a user of the dark themes, I want the same, so that legibility is a property of Redwall rather than of which theme I picked.
- As a maintainer, I want the contrast of every Redwall over every theme asserted in the test suite, so that a new wallpaper cannot ship an unreadable overlay.
- As a maintainer, I want the render to be deterministic, so that the same art and the same state always produce the same bytes and a diff means something changed.
- As a maintainer, I want the font embedded in the binary, so that Redwall draws identically on a machine where the user picked a different terminal font.
- As a maintainer, I want the embedded font subset vendored and locked like the brand assets, so that its provenance and licence are recorded rather than assumed.
- As a maintainer, I want Redwall images kept out of the wallpapers directory, so that the existing sweep can still tell a retired wallpaper from one the user chose by hand.
- As a maintainer, I want a
red-dev redwall subcommand, so that the hook has one thing to call and the behaviour can be tested without a desktop.
- As a maintainer, I want the daemon's payload version checked, so that a RedSkills release that changes the contract degrades instead of drawing nonsense.
- As a user whose daemon speaks a newer protocol, I want Redwall to fall back to showing only the address, so that a version skew costs me the Worker count and not the whole feature.
- As a server operator, I want Redwall never to run on a headless machine, so that a provisioning tool does not generate images nothing will display.
- As a GNOME user on a shell that ignores the lock-screen key, I want the desktop background to work anyway, so that the feature is not all-or-nothing.
- As a Windows user, I want the desktop background to work, so that Redwall is useful even though the lock screen is not reachable there.
- As a user, I want no security prompt on first use, so that enabling a cosmetic feature does not look like installing something untrusted.
- As a maintainer, I want Redwall regenerated on state change rather than on a timer, so that an idle machine does no work.
- As a maintainer, I want the trigger to come from a general hook system in RedSkills, so that red-dev is one consumer among others rather than a special case wired into the daemon.
- As a developer reading the glossary, I want Redwall defined in the
visual context, so that the term means one thing across the repo.
- As a user uninstalling red-dev, I want generated Redwall images removed, so that turning the feature off leaves no orphan files.
- As a maintainer, I want the Redwall directory separate and disposable, so that clearing it is safe and never touches a user's chosen wallpaper.
Human Decisions
-
Decision: The feature is called Redwall.
Why: The maintainer named it directly, rejecting all four proposed alternatives.
Alternatives considered: Living Wallpaper, Dynamic Wallpaper, Status Wallpaper, Heads-Up Wallpaper — all declined in favour of a brand-shaped name.
-
Decision: Redwall covers both the desktop background and the lock screen, and the user chooses whether it is on.
Why: The maintainer wanted both surfaces, not wallpaper alone.
Alternatives considered: Wallpaper only, which was the agent's recommendation on the grounds that GNOME and Windows screensavers are two different projects; overridden.
-
Decision: Composing live data over the brand art does not contradict the decision recorded in src/wallpaper.ts.
Why: That decision removed a generator that replaced the art with a gradient; Redwall keeps the mark and writes on top of it.
Alternatives considered: Confining data to a band that never touches the mark; reopening the decision as an ADR. Both judged unnecessary.
-
Decision: Redwall shows the LAN address.
Why: Chosen directly over the alternatives.
Alternatives considered: The WSL 172.x address, showing both labelled, or deriving it from what SSH answers on.
-
Decision: The address is chosen by the default route, with the option to pin one later.
Why: The maintainer took the default-route rule and asked that pinning remain available.
Alternatives considered: An exclusion list of virtual adapters, or showing every Up interface.
-
Decision: The trigger is a general hook and OS-notification system to be built in RedSkills, which red-dev then subscribes to with an image-generation hook.
Why: The maintainer rejected every option offered and specified a general mechanism instead of a point solution, to be filed as a separate ticket in ../red-skills.
Alternatives considered: red-dev subscribing to the daemon socket directly; red-dev polling host-state; the daemon generating the image itself — the agent's recommendation, declined.
-
Decision: The overlay is drawn by a TTF rasteriser written in TypeScript.
Why: Chosen over the agent's recommendation of a bitmap atlas.
Alternatives considered: An embedded bitmap glyph atlas (recommended, declined); SVG rendered natively by GNOME, which has no Windows counterpart; ImageMagick declared in the manifest, rejected as a heavy dependency with no Windows package.
-
Decision: The hook carries the full host-state payload.
Why: Chosen over a bare signal.
Alternatives considered: A signal with no payload, leaving red-dev to fetch host-state itself — the agent's recommendation, declined; a minimal payload of just the Worker count.
-
Decision: Windows gets the desktop background only; the lock screen is a Linux capability.
Why: Accepted after evidence that the target machine is Windows 11 Home, where the lock-screen policy is not available, and that an unsigned .scr triggers SmartScreen on every new machine.
Alternatives considered: A signed .scr (certificate cost), an unsigned .scr (SmartScreen friction), or attempting the registry path and degrading silently. This supersedes the earlier choice of a Windows .scr.
-
Decision: The user-facing setting follows the existing convention for theme, font and ble.sh.
Why: Stated as "the same as we do with everything else" — first-run question, recorded in preferences, changeable from red-dev menu.
-
Decision: Redwall is off by default.
Why: Accepted on the argument that writing over someone's wallpaper unasked is the same overreach ADR 0003 reversed for the terminal.
Implementation Decisions
- A new
redwall module owns composition. Its core is a pure function from base art plus machine state to PNG bytes; everything with an effect — reading host-state, resolving the address, writing the file, applying it — sits outside it.
- A new
red-dev redwall subcommand is what the RedSkills hook invokes. It is a command with its own lifecycle rather than an extension of red-dev theme, which must not acquire a responsibility that fires on its own.
- Redwall joins
THEME_SURFACES in theme-apply.ts as an additional surface, so a theme switch regenerates it through the path that already exists. It applies only when the preference is on and p.env !== "server".
- Generated images live in their own directory, separate from
wallpapers/. The wallpapers are immutable per theme and content-addressed by an eight-hex sha256 prefix; a Redwall changes whenever state changes, and sharing that directory would make expectedWallpaperNames() unbounded and destroy the sweep's ability to tell a retired image from a hand-chosen one.
- Glyphs come from a subset of FiraCode Nerd Font, vendored and pinned in the manner of
vendor/brand/brand.lock.json — commit, nearest tag, and a why, with the OFL text travelling alongside. Only the characters Redwall draws are included. The subset is embedded in the binary with with { type: "file" }, the same mechanism the six wallpapers use, so scripts/embed-smoke.ts fails the build if it goes missing.
- Text colour is chosen from the theme's declared
appearance, which themes.ts already carries as "light" | "dark". No pixel sampling.
- The address is resolved from the default route. Under WSL this means the Windows host's address, not the distro's — the
eth0 address is behind NAT and routes to nothing. A preference may pin a specific interface, with the default route as the fallback.
- Payload version is pinned and degrades.
host-state carries version and protocol_version. A payload whose version is not understood produces a Redwall showing the address alone; the Worker count is dropped rather than the whole overlay, matching the behaviour when the daemon is not running at all.
- The lock screen is set through
org.gnome.desktop.screensaver picture-uri. Where the shell ignores it, that is accepted rather than worked around; a GNOME Shell extension is a component with its own lifecycle and is out of scope.
- The preference joins
preferences.ts alongside theme, font, fontSize and blesh, is asked during first run, and is changeable from red-dev menu.
- A separate ticket in
reddb-io/red-skills must land first: a general hook and OS-notification system in the daemon. Redwall cannot be triggered until it exists. This Spec depends on it and does not specify it.
Testing Decisions
A good test here asserts what a person could observe — the bytes rendered, the surface invoked, the contrast achieved — and never how the module reached them. Four of the five seams already exist; only the renderer is new, and it is new at the highest point that can exercise the rule.
- Deterministic render — the new seam.
render(art, state) → bytes is pure, so the same art and state must produce identical bytes. This is where Worker counts, a missing address, and version degradation are exercised without touching disk, gsettings, or the Windows registry.
THEME_SURFACES injection — theme-apply.ts already accepts an injected surface list and theme-apply.test.ts already has a spy that records what each surface receives. Redwall's presence when enabled, and its absence when disabled or on a server, are asserted through that existing spy. Prior art: the surface tests added when ADR 0003 removed nine surfaces.
- Contrast as a build gate —
brand.ts already exports contrast(), and theme-contrast.test.ts already asserts ratios in a table. Redwall's text over each of the six themes joins that table, so an unreadable overlay is a red build rather than a visual judgement. Prior art: brand-guardrail.test.ts, which reads its expectations out of the vendored tokens instead of transcribing them.
- Vendored font declared and locked — the pattern from
brand-lock.test.ts and from what init.sh sources, the manifest installs in manifest.test.ts: an asset used by the code must be declared and pinned, so deleting one side fails the test.
host-state as a fixture — a captured real payload under src/fixtures/, where the repo already keeps this kind of thing. Version degradation is tested by changing version in the fixture.
Deliberately untested: real gsettings and real SystemParametersInfo. theme-apply.test.ts already treats these as observed surfaces, and a test that drives GNOME does not run in CI.
Acceptance Criteria
Out of Scope
- The RedSkills hook and OS-notification system. A separate ticket in
reddb-io/red-skills. Redwall consumes it and does not define it; until it lands, Redwall has no trigger.
- A Windows
.scr screensaver. Ruled out on SmartScreen friction and certificate cost.
- The Windows lock screen. Not reachable by policy on Home editions.
- A GNOME Shell extension to force the lock-screen image where the shell ignores the key.
- macOS.
providerFor fails closed on Darwin, and ADR 0001 reserves it for Phase 5 as an adapter of the Phase 4 contracts.
- Any data beyond the Worker count and the address. Further fields are a later conversation.
- Animation or per-frame updates. Redwall is a still image regenerated on state change.
Further Notes
The term Redwall is defined in .red/contexts/visual/CONTEXT.md under the visual context, landed via PR #51.
Two prior decisions constrain this work and were checked rather than assumed. src/wallpaper.ts records that a wallpaper generator was deliberately removed, because it could not draw the brand mark and because a bug in it was silent; Redwall is compatible with that only for as long as it composes over the art rather than replacing it. ADR 0003 records that red-dev stopped colouring the terminal because those colours were not red-dev's to choose; the same reasoning is why Redwall is off by default.
One measurement worth carrying forward: on the maintainer's machine the Windows host reports three IPv4 addresses — the LAN address on an Up Ethernet adapter, the WSL virtual switch, and a disconnected APIPA address. Any implementation that picks by index rather than by route will pick wrong.
Problem Statement
A machine running red-dev knows things about itself that its owner has to go and ask for. How many AFK Workers are grinding right now lives behind
redskilled statuslinein a terminal. Which address the machine answers on lives behindip addron one side of the WSL boundary andGet-NetIPAddresson the other, and the two disagree.Both are glanceable facts — the kind you want to read across the room, or on a locked screen, without unlocking anything or opening a terminal. Today the desktop shows brand art and nothing else, and the state is a command away at exactly the moment you cannot type one.
Solution
Redwall: the theme's wallpaper with live machine state drawn over it.
The brand art underneath is unchanged — Redwall composes on top of it, never in place of it. It reaches two surfaces from one image: the desktop background, and the GNOME lock screen where the shell honours it. What it carries is the count of active Workers and the address this machine answers on.
It is off by default and turned on the way a theme or a font is: asked once at first run, recorded in preferences, changeable later from the menu. It regenerates when the state it displays changes, driven by a hook the RedSkills daemon fires — not by a clock.
User Stories
172.xNAT address that no other host can route to.red-dev menu, so that I do not have to reinstall to change my mind.red-dev redwallsubcommand, so that the hook has one thing to call and the behaviour can be tested without a desktop.visualcontext, so that the term means one thing across the repo.Human Decisions
Decision: The feature is called Redwall.
Why: The maintainer named it directly, rejecting all four proposed alternatives.
Alternatives considered: Living Wallpaper, Dynamic Wallpaper, Status Wallpaper, Heads-Up Wallpaper — all declined in favour of a brand-shaped name.
Decision: Redwall covers both the desktop background and the lock screen, and the user chooses whether it is on.
Why: The maintainer wanted both surfaces, not wallpaper alone.
Alternatives considered: Wallpaper only, which was the agent's recommendation on the grounds that GNOME and Windows screensavers are two different projects; overridden.
Decision: Composing live data over the brand art does not contradict the decision recorded in
src/wallpaper.ts.Why: That decision removed a generator that replaced the art with a gradient; Redwall keeps the mark and writes on top of it.
Alternatives considered: Confining data to a band that never touches the mark; reopening the decision as an ADR. Both judged unnecessary.
Decision: Redwall shows the LAN address.
Why: Chosen directly over the alternatives.
Alternatives considered: The WSL
172.xaddress, showing both labelled, or deriving it from what SSH answers on.Decision: The address is chosen by the default route, with the option to pin one later.
Why: The maintainer took the default-route rule and asked that pinning remain available.
Alternatives considered: An exclusion list of virtual adapters, or showing every
Upinterface.Decision: The trigger is a general hook and OS-notification system to be built in RedSkills, which red-dev then subscribes to with an image-generation hook.
Why: The maintainer rejected every option offered and specified a general mechanism instead of a point solution, to be filed as a separate ticket in
../red-skills.Alternatives considered: red-dev subscribing to the daemon socket directly; red-dev polling
host-state; the daemon generating the image itself — the agent's recommendation, declined.Decision: The overlay is drawn by a TTF rasteriser written in TypeScript.
Why: Chosen over the agent's recommendation of a bitmap atlas.
Alternatives considered: An embedded bitmap glyph atlas (recommended, declined); SVG rendered natively by GNOME, which has no Windows counterpart; ImageMagick declared in the manifest, rejected as a heavy dependency with no Windows package.
Decision: The hook carries the full
host-statepayload.Why: Chosen over a bare signal.
Alternatives considered: A signal with no payload, leaving red-dev to fetch
host-stateitself — the agent's recommendation, declined; a minimal payload of just the Worker count.Decision: Windows gets the desktop background only; the lock screen is a Linux capability.
Why: Accepted after evidence that the target machine is Windows 11 Home, where the lock-screen policy is not available, and that an unsigned
.scrtriggers SmartScreen on every new machine.Alternatives considered: A signed
.scr(certificate cost), an unsigned.scr(SmartScreen friction), or attempting the registry path and degrading silently. This supersedes the earlier choice of a Windows.scr.Decision: The user-facing setting follows the existing convention for theme, font and ble.sh.
Why: Stated as "the same as we do with everything else" — first-run question, recorded in preferences, changeable from
red-dev menu.Decision: Redwall is off by default.
Why: Accepted on the argument that writing over someone's wallpaper unasked is the same overreach ADR 0003 reversed for the terminal.
Implementation Decisions
redwallmodule owns composition. Its core is a pure function from base art plus machine state to PNG bytes; everything with an effect — readinghost-state, resolving the address, writing the file, applying it — sits outside it.red-dev redwallsubcommand is what the RedSkills hook invokes. It is a command with its own lifecycle rather than an extension ofred-dev theme, which must not acquire a responsibility that fires on its own.THEME_SURFACESintheme-apply.tsas an additional surface, so a theme switch regenerates it through the path that already exists. It applies only when the preference is on andp.env !== "server".wallpapers/. The wallpapers are immutable per theme and content-addressed by an eight-hex sha256 prefix; a Redwall changes whenever state changes, and sharing that directory would makeexpectedWallpaperNames()unbounded and destroy the sweep's ability to tell a retired image from a hand-chosen one.vendor/brand/brand.lock.json— commit, nearest tag, and awhy, with the OFL text travelling alongside. Only the characters Redwall draws are included. The subset is embedded in the binary withwith { type: "file" }, the same mechanism the six wallpapers use, soscripts/embed-smoke.tsfails the build if it goes missing.appearance, whichthemes.tsalready carries as"light" | "dark". No pixel sampling.eth0address is behind NAT and routes to nothing. A preference may pin a specific interface, with the default route as the fallback.host-statecarriesversionandprotocol_version. A payload whose version is not understood produces a Redwall showing the address alone; the Worker count is dropped rather than the whole overlay, matching the behaviour when the daemon is not running at all.org.gnome.desktop.screensaver picture-uri. Where the shell ignores it, that is accepted rather than worked around; a GNOME Shell extension is a component with its own lifecycle and is out of scope.preferences.tsalongsidetheme,font,fontSizeandblesh, is asked during first run, and is changeable fromred-dev menu.reddb-io/red-skillsmust land first: a general hook and OS-notification system in the daemon. Redwall cannot be triggered until it exists. This Spec depends on it and does not specify it.Testing Decisions
A good test here asserts what a person could observe — the bytes rendered, the surface invoked, the contrast achieved — and never how the module reached them. Four of the five seams already exist; only the renderer is new, and it is new at the highest point that can exercise the rule.
render(art, state) → bytesis pure, so the same art and state must produce identical bytes. This is where Worker counts, a missing address, and version degradation are exercised without touching disk, gsettings, or the Windows registry.THEME_SURFACESinjection —theme-apply.tsalready accepts an injected surface list andtheme-apply.test.tsalready has a spy that records what each surface receives. Redwall's presence when enabled, and its absence when disabled or on a server, are asserted through that existing spy. Prior art: the surface tests added when ADR 0003 removed nine surfaces.brand.tsalready exportscontrast(), andtheme-contrast.test.tsalready asserts ratios in a table. Redwall's text over each of the six themes joins that table, so an unreadable overlay is a red build rather than a visual judgement. Prior art:brand-guardrail.test.ts, which reads its expectations out of the vendored tokens instead of transcribing them.brand-lock.test.tsand fromwhat init.sh sources, the manifest installsinmanifest.test.ts: an asset used by the code must be declared and pinned, so deleting one side fails the test.host-stateas a fixture — a captured real payload undersrc/fixtures/, where the repo already keeps this kind of thing. Version degradation is tested by changingversionin the fixture.Deliberately untested: real
gsettingsand realSystemParametersInfo.theme-apply.test.tsalready treats these as observed surfaces, and a test that drives GNOME does not run in CI.Acceptance Criteria
bun testpasses with the new Redwall tests included, andtsc --noEmitis clean.render(art, state)returns byte-identical output for identical inputs across two invocations.theme-applyspy.p.env === "server".host-stateversion produces an overlay carrying the address and no Worker count.red-dev redwallexits zero on a machine with the preference off, changing nothing.expectedWallpaperNames()is unchanged by Redwall generation.red-dev uninstallremoves the Redwall directory, asserted by a test.Out of Scope
reddb-io/red-skills. Redwall consumes it and does not define it; until it lands, Redwall has no trigger..scrscreensaver. Ruled out on SmartScreen friction and certificate cost.providerForfails closed on Darwin, and ADR 0001 reserves it for Phase 5 as an adapter of the Phase 4 contracts.Further Notes
The term Redwall is defined in
.red/contexts/visual/CONTEXT.mdunder thevisualcontext, landed via PR #51.Two prior decisions constrain this work and were checked rather than assumed.
src/wallpaper.tsrecords that a wallpaper generator was deliberately removed, because it could not draw the brand mark and because a bug in it was silent; Redwall is compatible with that only for as long as it composes over the art rather than replacing it. ADR 0003 records that red-dev stopped colouring the terminal because those colours were not red-dev's to choose; the same reasoning is why Redwall is off by default.One measurement worth carrying forward: on the maintainer's machine the Windows host reports three IPv4 addresses — the LAN address on an
UpEthernet adapter, the WSL virtual switch, and a disconnected APIPA address. Any implementation that picks by index rather than by route will pick wrong.