skills/event-sourcing-state/SKILL.md
Event-sourced application state pattern for TypeScript apps. Prefer bounded event logs plus pure derivation functions over mirrored mutable lifecycle flags. Use when state transitions are driven by events and bugs can be reproduced from a saved event stream.
npx skillsauth add remorses/kimaki event-sourcing-stateInstall 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.
Use this skill when an app keeps adding mutable fields to track lifecycle, phase, status, or UI state that could instead be derived from an event log.
Do not store the answer when you can store the evidence.
Coding agents overproduce state. Every bug looks like it wants one more flag, one more cached answer, one more special case. Every field feels locally justified. Globally you are building a machine nobody can hold in their head.
Every boolean you add:
The fix is not a better set of flags. The fix is deleting the flags.
Stop storing conclusions and store evidence instead. If a decision depends on what actually happened, keep the events and derive the answer from them.
To answer one yes/no UI question ("should the footer show?"), an agent will mirror facts into state:
type ThreadState = {
wasInterrupted: boolean
didAssistantFinish: boolean
didAssistantError: boolean
wasToolCallOnly: boolean
}
function shouldShowFooter(state: ThreadState): boolean {
return state.didAssistantFinish
&& !state.wasInterrupted
&& !state.didAssistantError
&& !state.wasToolCallOnly
}
Four flags to answer one question. Each flag caches a fact already present in the event that produced it. Then a function recombines them back into one boolean. None of these fields looks insane on its own — that is the trap.
Keep the raw events and compute the answer when needed:
type SessionEvent =
| { type: 'session.status'; status: 'busy' | 'idle' }
| { type: 'session.aborted' }
| {
type: 'message.updated'
role: 'assistant'
completed: boolean
error: boolean
finish: 'stop' | 'tool-calls'
}
function getLatestAssistantMessage(events: SessionEvent[]) {
for (let i = events.length - 1; i >= 0; i--) {
const event = events[i]
if (event?.type === 'message.updated' && event.role === 'assistant') {
return event
}
}
return undefined
}
function isNaturalCompletion(message: {
completed: boolean
error: boolean
finish: 'stop' | 'tool-calls'
}): boolean {
if (!message.completed) {
return false
}
if (message.error) {
return false
}
return message.finish !== 'tool-calls'
}
function shouldShowFooter(events: SessionEvent[]): boolean {
const msg = getLatestAssistantMessage(events)
if (!msg) {
return false
}
return isNaturalCompletion(msg)
}
Notice what disappeared:
You keep the raw thing that happened, then compute the answer when needed.
Any model can one-shot these problems because the feedback loop is obvious: events in, answer out.
import fs from 'node:fs'
function loadEvents(file: string): SessionEvent[] {
return fs
.readFileSync(file, 'utf8')
.split('\n')
.filter(Boolean)
.map((line) => {
return JSON.parse(line) as SessionEvent
})
}
test('footer is hidden for aborted runs', () => {
const events = loadEvents('./fixtures/aborted-session.jsonl')
expect(shouldShowFooter(events)).toBe(false)
})
The reproduction artifact is just data:
If you want persistence you just store the events. Events are easily versioned and type-safe.
The trade is this:
State is cached conclusions. Events are stored evidence. Evidence ages better.
If you can derive it, don't store it.
The next best thing after no state is state you don't care about because it is encapsulated.
Not everything needs event sourcing. The second-best option is state you
successfully hide. A good example is React useState: state can only be
written in event handlers within the component subtree and can only be read in
the current component. It is local and easy to reason about.
The same applies to backend code. Instead of promoting a timer or counter into a class field visible to all methods, encapsulate it in a closure:
// bad: timer is a class field, visible to all methods, agents will touch it
class MessageWriter {
private debounceTimeout: ReturnType<typeof setTimeout> | null = null
queueSend(text: string): void {
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
this.debounceTimeout = setTimeout(() => {
this.write(text)
}, 300)
}
}
// good: timer is trapped in a tiny box, no other consumer can touch it
function createDebouncedAction(callback: () => void, delayMs = 300) {
let timeout: ReturnType<typeof setTimeout> | null = null
function clear(): void {
if (!timeout) {
return
}
clearTimeout(timeout)
timeout = null
}
function trigger(): void {
clear()
timeout = setTimeout(() => {
timeout = null
callback()
}, delayMs)
}
return { trigger, clear }
}
A global variable has the potential of doubling your app state. An encapsulated closure can only double the states of that tiny function. Given it is so small you don't care — spotting a bug inside it is easy for you and agents.
documentation
Best practices for creating a SKILL.md file. Covers file structure, frontmatter, writing style, and where to place skills in a repository. Use when the user wants to create a new skill, update an existing skill, write a SKILL.md, or asks how skills work.
development
Opinionated TypeScript npm package template for ESM packages. Enforces src→dist builds with tsc, strict TypeScript defaults, explicit exports, and publish-safe package metadata. Use this when creating or updating any npm package in this repo.
documentation
Best practices for creating a SKILL.md file. Covers file structure, frontmatter, writing style, and where to place skills in a repository. Use when the user wants to create a new skill, update an existing skill, write a SKILL.md, or asks how skills work.
tools
Centralized state management pattern using Zustand vanilla stores. One immutable state atom, functional transitions via setState(), and a single subscribe() for all reactive side effects. Based on Rich Hickey's "Simple Made Easy" principles: prefer values over mutable state, derive instead of cache, centralize transitions, and push side effects to the edges. Resource co-location in the same store is also valid when lifecycle management is safer that way. Also covers state encapsulation: keeping state local to its owner (closures, plugins, factory functions) so it doesn't leak across the app, reducing the blast radius of mutations. Also covers event sourcing: keeping a bounded event buffer and deriving state with pure functions instead of mutable flags, making event handlers easy to test and reason about. Use this skill when building any stateful TypeScript application (servers, extensions, CLIs, relays) to keep state simple, testable, and easy to reason about. ALWAYS read this skill when a project uses zustand/vanilla for state management outside of React.