.claude/skills/architectural-invariants/SKILL.md
Catalog of cross-cutting architectural invariants that code must respect. Use when designing, implementing, or reviewing features that involve shared resources, persistence, or I/O symmetry. Ask each catalog question against the change.
npx skillsauth add ms2sato/agent-console architectural-invariantsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill is a catalog of meta-invariants — cross-cutting architectural rules whose violation produces a characteristic class of bugs. Each invariant is stated abstractly, then followed by detection heuristics, concrete examples, and resolution patterns.
The catalog is deliberately short. Each entry is a high-leverage pattern whose violation is easy for humans to miss and hard to catch with per-file review.
Rule. For any persistent resource that is both written and read, the functions that determine where to write and where to read must produce the same value for the same identity — or their divergence must be explicit and justified.
Important caveat. "Write address = read address" is the common case, but not universal. Legitimate asymmetric designs include:
The invariant is not "address must be textually equal" — it is "for the same identity, the read must find the data that was written, modulo the consistency model the design explicitly commits to". Asymmetry is fine when documented and bounded. Accidental asymmetry is the bug.
Why it matters. If the write-address and read-address diverge for the same identity without an explicit consistency model, you get silent fragmentation: writes go to location A, reads come from location B. The system "works" on the write side (no error), but reads return stale or missing data. Every reconnection/restart amplifies the problem.
| Domain | Write side | Read side |
|--------|-----------|----------|
| Filesystem | writeFile(path) | readFile(path) — paths must converge per identity |
| Database | INSERT INTO table_a | SELECT FROM table_b — table identity must match |
| Cache | cache write key | cache read key |
| Message queue | publish topic | subscribe topic |
| Distributed storage | write consistency level + target | read consistency level + target |
| Cookie/Auth | Set-Cookie domain/path | request cookie scope |
| Log aggregation | log sink | query source |
| URL scheme | redirect URL | receiver URL |
| Memory / mmap | write offset calculation | read offset calculation |
writeFile, INSERT, cache.set, publish, etc.), find the corresponding read-side call (readFile, SELECT, cache.get, subscribe). Is the addressing function the same? If computed, do both compute identically??? defaultPath or || fallbackKey in the function that computes an address is a red flag — it means the address can differ based on runtime state.new PathResolver(...) or new KeyBuilder(...) appears in many places with different arguments, check that the arguments always resolve to the same value for the same identity.CanonicalOutputPath vs generic string). Reviewers notice when that type is assembled by hand.Issue #631. Worker output files fragmented across three directories because SessionDataPathResolver re-looked up repositoryName on each construction, silently fell back to _quick/ when the lookup returned undefined. Writes and reads both went through the resolver, but the resolver produced different paths depending on when it was called.
Review question that would have caught it: "The getOutputFilePath(sessionId) function is called on both the write side (PTY output flush) and the read side (history reconnect). Does it always return the same value for the same sessionId, across server restarts?"
The answer at the time was "no" — and that single question surfaces the entire class of bug.
Rule. If a value is computed from inputs via a rule, exactly one function implements that rule. All other code that needs the value calls that function — never reimplements the computation.
Why it matters. Duplicated computation drifts. Ten callers, ten subtly different copies of the logic → each evolves independently → silent behavior divergence. Especially dangerous when the computation involves path joining, key derivation, URL building, timestamp formatting, etc.
path.join(config, 'subdir', id) appears in 3+ places, someone is rolling their own copy of a computation.?? default in computation. If the default differs between call sites, the function is not canonical.compute<Thing>(inputs), callers are duplicating.computeSessionDataBaseDir).Rule. A value that identifies a resource (sessionId, userId, filePath-from-identity, DB primary key) must remain stable across the resource's entire lifetime — including server restarts, process crashes, DB restores, and config migrations.
Why it matters. Identifiers are used in client caches, URL bookmarks, cross-system references. If the identifier changes for the same underlying resource, every consumer that cached the old identifier is broken.
id = f(configRoot, userName, timestamp) and any input can change, the identifier is not stable.Rule. Any state the user expects to survive a server restart MUST be persisted to durable storage before the operation that produced it returns success.
Why it matters. In-memory state is lost on crash/restart. If a user sees "success" for an operation but the state was only in memory, they've been lied to.
void persist(data) without awaiting the result.Rule. Application state that affects behavior visible to other users (session status, worker output, templates, memos, etc.) lives on the server. Client state is either a cache of server state or purely transient UI state (dark mode, scroll position).
Why it matters. When the client owns state that should be shared, multi-device/multi-session flows break. Refreshes lose data. Collaboration becomes impossible.
localStorage.setItem holding user-meaningful data. Templates, drafts, session config → should be server-backed.localStorage restricted to transient UI preferences.localStorage is used only for transient UI preferences (if at all) → integration test covering reload/cross-session consistencyRule. Any value crossing a trust boundary (user input, external API response, job payload reconstructed after persistence, cross-process IPC) is validated before use.
Why it matters. Values from outside are not type-safe even if TypeScript says they are. A corrupted DB value can still type-check as string. A job payload from disk can contain any bytes. Trust the type system only for values that never left your process.
JSON.parse(body) used without v.parse(Schema, ...).path.join(userInput, ...) can escape the intended directory.startsWith(allowedRoot).A new entry to this catalog should satisfy all of:
Format: I-<N>. <Short name>, followed by rule, why, detection heuristics, resolution patterns, concrete example.
code-quality-standards — per-code-unit quality (SRP, readability, etc.). Complementary: this skill is about cross-unit invariants.test-standards — how to test. Complementary: this skill names what MUST be tested (e.g., identity stability across restart).orchestrator/acceptance-check.js — runs this catalog as a required question during acceptance.orchestrator/delegation-prompt.js — injects relevant invariants into the agent's task prompt based on Issue content.testing
UX design principles for agent-console. Use when designing features, evaluating acceptance criteria, or reviewing user-facing interactions in a multi-agent management UI.
development
Detailed test patterns and code examples. Use when you need step-by-step testing guidance, Server Bridge Pattern, or concrete code patterns beyond what the auto-loaded testing rules provide.
development
Orchestrator role for strategic decision-making, task prioritization, parallel task coordination via worktree delegation, and first-responder for dev agent questions. Use when managing multiple development agents or making prioritization decisions.
development
Detailed React patterns and code examples for frontend implementation. Use when you need step-by-step guidance or concrete code patterns beyond what the auto-loaded frontend rules provide.