Read CLAUDE.md first. This file adds agent-specific rules, decision trees, and file ownership on top of it.
Every module, every page, every group has one registration point. When asked to add, change, or remove any of these, touch the registry entry — let the rest of the system derive from it. Do not scatter the change across surface files.
If the registry entry does not exist, create it before writing surface code.
// NEVER add fields like these to ArcadiaRoot or any surface state
pub shell_enabled: bool,
pub lan_enabled: bool,
pub net_enabled: bool,
// NEVER add methods like these
fn shell_enabled(&self) -> bool { … }
fn net_enabled(&self) -> bool { … }One method: fn is_module_enabled(&self, name: &str) -> bool. Query it with the *_MODULE_NAME constants from config/modules.rs.
// NEVER write this pattern
match page_id {
"utility.shell" => self.shell_enabled,
"network.overview" => self.net_enabled(),
_ => true,
}Page visibility derives from NavigationPageDefinition.required_module, not surface-level match arms.
// NEVER grow this pattern
if self.active_page_id == "utility.shell" { … }
else if self.active_page_id == "global.modules" { … }
else if self.active_page_id == "network.overview" { … } // don't addIf this pattern exists and you must add a page, flag it as technical debt before extending it.
// NEVER hardcode magic behavior on specific page IDs in generic handlers
.onChange(of: activePageID) { pageID in
if pageID == "global.modules" { reloadModules() }
}Lifecycle side-effects belong in the view for that page (e.g. ModulesView.onAppear), not in a global observer.
// NEVER inline hex colors in app.rs or any view file
.bg(rgb(0x151a22))
.text_color(rgb(0x93c5fd))// NEVER inline colors in SwiftUI views
Color(hex: "151a22")Desktop colors: Desktop/src/gui/theme/mod.rs or component files under theme/modules/.
iOS colors: computed properties on AppTheme in AppTheme.swift.
// NEVER write the same business logic in both Desktop/src/gui/app/ AND Mobile/iOS/ArcadiaApp/
// If it belongs to both, it belongs in arcadia-core.
// NEVER create new remote-session.foo commands for UI mirroring.
// CORRECT — extend surface.snapshot extra fields and surface.patch ops.
The remote-session module is a routing gate only. surface.* is the protocol for UI state mirroring.
// NEVER rename a module name constant without adding a migration in:
// ModulesConfig::merge_defaults() in config/modules.rs
// Follow the LEGACY_LAN_MODULE_NAME pattern.
// NEVER commit ffi.rs changes without running:
// bash Shared/Scripts/build-ios-framework.sh
// and committing the updated Generated/ + ArcadiaCore.xcframework
// 1. Shared/ArcadiaCore/src/config/modules.rs — add constant + registry entry
pub const FOO_MODULE_NAME: &str = "foo";
static MODULE_REGISTRY: &[ModuleManifest] = &[
// … existing …
ModuleManifest {
name: FOO_MODULE_NAME,
version: "1.0.0",
description: "What foo does.",
required_modules: &[], // or &[NET_MODULE_NAME] etc.
},
];
// 2. Create Shared/ArcadiaCore/src/modules/foo.rs
pub fn commands() -> &'static [ModuleCommand] {
&[
ModuleCommand { token: "foo.bar", description: "Does bar." },
]
}
// 3. Register in Shared/ArcadiaCore/src/modules/mod.rs
// Done — GUI, CLI, iOS module list updates automatically// 1. Shared/ArcadiaCore/src/navigation.rs — add to PAGE_DEFINITIONS
NavigationPageDefinition {
id: "utilities.foo",
title: "Foo",
description: "Foo does things.",
glyph: "foo", // must have a matching arm in icon_path()
system_image: "star", // SF Symbol for iOS
accent: "emerald",
required_module: Some(FOO_MODULE_NAME), // or None if always visible
},
// 2. Add "utilities.foo" to GROUP_DEFINITIONS.pages for the relevant group
// 3. Desktop: add panel render + route via page ID (derive visibility from required_module)
// 4. iOS: add view + route in ContentView page dispatch// Rust (Desktop) — use MODULE_NAME constants, not string literals
fn is_module_enabled(&self, name: &str) -> bool {
self.module_rows.iter()
.find(|(n, _)| n == name)
.map(|(_, enabled)| *enabled)
.unwrap_or(false)
}
// Call as: self.is_module_enabled(SHELL_MODULE_NAME)// Swift (iOS)
func isModuleEnabled(_ name: String) -> Bool {
modules.first(where: { $0.name == name })?.enabled ?? false
}
// Call as: isModuleEnabled(ModuleNames.shell)// 1. modules/surface.rs — extend SurfaceSnapshot.extra
// 2. modules/surface.rs — add SurfacePatch variant if clients push changes back
// 3. Desktop + iOS surfaces consume new extra field from snapshot result
// 4. Do NOT create remote-session.foo verbs — keep protocol under surface.*// 1. Edit MODULE_REGISTRY entry + constant in config/modules.rs
// 2. Add migration in ModulesConfig::merge_defaults():
const LEGACY_FOO_NAME: &str = "foo-old";
if let Some(val) = self.modules.remove(LEGACY_FOO_NAME) {
self.modules.entry(FOO_MODULE_NAME.to_string()).or_insert(val);
}
// 3. Done — no ad-hoc renames at call sitesAsk these questions. If any answer is "no," stop and fix it first.
- Does a registry entry exist for this? → If not, create it before touching surface code.
- Am I adding a name check on a specific module or page ID in surface code? → If yes, that logic belongs in the registry declaration or the core.
- Am I adding a new field/property that tracks a specific module's state? → Use
is_module_enabled(name)instead. - Am I writing the same logic for both Desktop and iOS? → Move it to
arcadia_core. - Am I inlining a color value? → Put it in the theme layer.
- Did I change
ffi.rsor any FFI-exported type? → Runbuild-ios-framework.shbefore committing. - Am I renaming a module? → Add a
merge_defaults()migration. - Am I creating a new
remote-session.*command for UI state? → Usesurface.snapshot/surface.patchinstead.
Do not. If time is the constraint, implement the proper registry-driven pattern and leave a // TODO: <why this is debt> comment — do not leave hardcoded strings in surface logic. A hardcoded page ID match arm today becomes five hardcoded match arms after the next change touches the file.
| File | Purpose | Agent rule |
|---|---|---|
config/modules.rs |
Module registry + config + migrations | Extend MODULE_REGISTRY; add migrations to merge_defaults(); never add per-module booleans |
navigation.rs |
Page/group registry + JSON serialization | Extend PAGE_DEFINITIONS / GROUP_DEFINITIONS; never add parallel lists |
ffi.rs |
UniFFI bridge | After changes: rebuild xcframework; commit Generated/ |
modules/surface.rs |
Snapshot / patch / revision | Extend extra + SurfacePatch; do not create ad-hoc remote-session.* verbs |
modules/remote_mirror.rs |
Host transcript queue + FFI drain | For inbound NODE_EXEC mirroring only |
modules/shell.rs, modules/lan/, etc. |
Module command handlers | One file per module; no cross-module logic |
gui/app/mod.rs |
Desktop root state (ArcadiaRoot) |
No per-module booleans; no hardcoded page IDs |
gui/theme/mod.rs |
Desktop icon + color helpers | All Desktop color/icon lookups; never inline in views |
gui/tui/ |
PTY/TUI terminal emulator | Desktop-specific; no equivalent on iOS (shell.execute only) |
AppTheme.swift |
iOS color tokens | All iOS colors as computed properties |
ContentView.swift |
iOS coordinator | Thin — registry + module state consumer; no business logic |
NavigationModels.swift |
Swift nav types | Mirror of Rust NavigationPageDefinition / NavigationGroupDefinition; update after navigation.rs changes |
ModuleNames.swift |
iOS module name constants | Mirror of MODULE_REGISTRY name fields; update after adding modules |
Before marking a feature ready for production, verify:
- New capability registered in
MODULE_REGISTRY(if module) orPAGE_DEFINITIONS(if page) - No hardcoded module/page IDs in surface visibility or dispatch logic
- No per-module boolean fields added to surface state structs
- No inline colors in view/render code
- FFI changes accompanied by
xcframeworkrebuild +Generated/commit - Module rename includes
merge_defaults()migration - New mirrored state uses
surface.*protocol, not ad-hoc verbs -
cargo test -p arcadia-corepasses - Known gap addressed or documented in
gaps.mdif not fully solved
- LAN command forwarding requires
remote-session+lan+netenabled locally. The peer checks its own module rules. surface.revisionis not a reliable freshness signal yet — gap 1 ingaps.md. Do not build logic that assumes revision covers all write paths.surface.patchclient_idis attribution only — not authentication. Do not build authorization logic on it.- Multiple concurrent clients patching the same host = last-writer-wins. Do not imply merge semantics.
| Capability | Desktop | iOS | Core |
|---|---|---|---|
| Shell (PTY/TUI) | Full PTY + TUI via gui/tui/ |
shell.execute only |
modules/shell.rs |
| Shell MOTD | Yes | Yes (via execute) | modules/shell_motd.rs |
| Module toggles | GUI + CLI | SwiftUI ModulesView |
FFI set_module_enabled* |
| LAN discovery | lan_nodes/ panel |
LanNodesView |
modules/lan/ |
| Surface snapshot | Yes | Yes | modules/surface.rs |
| Remote mirror drain | Yes (shell/mirror.rs) | Yes (250ms timer) | FFI drain_remote_mirror_batch |
| Thin-client route | Session chip in top bar | Route picker in sidebar | ThinClientConfig |
| Splash screen | Animated canvas (splash/) |
SplashView.swift |
— |
Divergence between surfaces is tracked in gap 10 of gaps.md. When implementing a new capability, prefer making it routable via execute_command so both surfaces can reach it over LAN without platform-specific implementations.