The Debug-First AI Workflow: Why I Make My Assistant Break Things on Purpose

Most people use AI assistants to write code. I’ve started using mine to break code first.

It sounds counterintuitive, but this one change to my workflow cut my bug rate in half and made code reviews actually meaningful.

The Problem With Write-First

The default AI coding workflow looks like this:

  1. Describe what you want
  2. AI writes the code
  3. You review it
  4. You find bugs (maybe)
  5. You fix them

Step 4 is where things fall apart. Reviewing AI-generated code is hard because it looks correct. It’s well-structured, properly indented, has reasonable variable names. Your brain pattern-matches it as “good code” and skips over the logic errors.

I call this correctness theater — the code performs competence without actually being correct.

The Debug-First Approach

Here’s what I do instead:

Step 1: Write the Spec (30 seconds)

Function: calculateShippingCost(order)
- Input: order with items[], destination, shipping_method
- Output: { cost: number, estimatedDays: number }
- Rules: free shipping over $100, express = 2x base rate,
  international adds flat $15, empty orders throw error

Step 2: Ask for Failure Scenarios (AI’s first job)

Given this spec, list 10 ways this function could fail or produce
wrong results. Be specific — include the input that would trigger
each failure.

AI returns something like:

  1. Empty items array → should throw, might return $0
  2. Order total exactly $100 → boundary: is it > or >=?
  3. Negative item prices → cost could go negative
  4. Missing destination → international check fails silently
  5. Express + international + free shipping → which rules apply in what order?
  6. Single item worth $101 → free shipping, but express is 2x of what?
  7. Floating point: $33.33 × 3 = $99.99, not $100
  8. Null shipping_method → undefined behavior
  9. destination = “” (empty string) → not null, not valid
  10. Items with quantity: 0 → included in total?

This takes 10 seconds and I already have a better understanding of my own spec than I did when I wrote it.

Step 3: Turn Failures Into Tests (AI’s second job)

Write a test for each failure scenario. Each test should currently FAIL
(because we haven't written the implementation yet). Use Vitest.

Now I have 10 tests that define what “correct” actually means — before a single line of implementation exists.

Step 4: Write the Implementation (AI’s third job)

Write calculateShippingCost() that passes all 10 tests.
Run through each test mentally and confirm it would pass.

The implementation is now constrained by real edge cases. The AI can’t take shortcuts because the tests are already there, waiting to catch them.

Step 5: Verify (Mine)

I run the tests. Usually 8-9 pass on the first try. The 1-2 that fail reveal genuine ambiguities in my spec that I need to decide on. That’s a feature — I’d rather resolve ambiguity now than in production.

Why This Works Better

You Review Tests, Not Code

Reading 10 test descriptions is faster and easier than reading 50 lines of implementation. If the tests cover the right cases, the implementation is almost certainly correct. If they don’t, you know exactly what’s missing.

AI Is Better at Breaking Than Building

Ironically, AI assistants are more reliable at finding problems than solving them. When you ask “what could go wrong?”, the model draws on thousands of bug reports and failure modes from its training data. When you ask “write correct code,” it draws on thousands of code samples — many of which contain the very bugs it could have warned you about.

The Spec Improves Before Implementation

Half the bugs I ship come from ambiguous specs, not bad code. The debug-first workflow forces spec clarification before I write anything. By step 3, I’ve already caught the “$100 boundary” and “express + free shipping” ambiguities.

Code Reviews Become Meaningful

When someone reviews my PR, they see the test names:

✓ throws on empty order
✓ free shipping at exactly $100
✓ rejects negative prices
✓ handles express + international combo

The reviewer immediately understands what the code does, what edge cases were considered, and what “correct” means. They can focus on what’s missing from the tests rather than trying to read implementation logic.

The Template

Here’s the prompt sequence I use every time:

Prompt 1 — Failure Analysis:

Here's my function spec: [spec]
List 10 specific ways this could fail or produce wrong results.
Include the exact input for each.

Prompt 2 — Test Generation:

Write a failing test for each scenario. Use [test framework].
Tests should fail because the function doesn't exist yet.

Prompt 3 — Implementation:

Write the function to pass all tests. After writing it,
walk through each test and explain why it passes.

Prompt 4 — Gap Check:

Look at the tests and implementation together.
Are there any edge cases the tests don't cover?

Four prompts. Usually takes 5-10 minutes total. The resulting code is more correct than anything I’d get from a single “write this function” prompt.

When to Skip This

This is overkill for trivial functions. I don’t debug-first a formatDate() helper. My rule of thumb: if the function has more than 2 conditional branches or touches external state (DB, API, file system), it gets the debug-first treatment.

Try this on your next feature. The first time all 10 tests pass on the first run, you’ll feel like you have a superpower.

Why I Stopped Using AI for Boilerplate (and What I Use It For Instead)

Hot take: the worst use of AI coding assistants is the thing most people use them for — generating boilerplate.

I spent three months auto-generating CRUD endpoints, form components, and config files. I felt productive. My git log was full of big commits. Then I looked at what I actually shipped, and I realized I’d been optimizing the wrong thing.

The Boilerplate Trap

Here’s what happens when you use AI for boilerplate:

  1. You prompt: “Generate a CRUD API for users with Express and Prisma”
  2. AI spits out 200 lines of working code
  3. You glance at it, looks fine, commit
  4. A week later, you find three inconsistencies with your existing endpoints
  5. You spend an hour fixing patterns the AI didn’t know about

The problem isn’t that AI generates bad boilerplate. It’s that boilerplate is the part of your codebase where consistency matters most — and AI is terrible at consistency unless you over-specify every detail.

You know what’s great at consistent boilerplate? A template. A snippet. A plop generator. A shell script. Deterministic tools for deterministic work.

What AI Is Actually Great At

After a lot of trial and error, here’s where AI assistants consistently save me real time:

1. Thinking Through Edge Cases

Here's my function that handles file uploads.
What edge cases am I missing? List them as a checklist.

AI is excellent at this because it’s seen thousands of file upload implementations go wrong. It’ll catch: empty files, duplicate names, MIME type mismatches, concurrent uploads, disk space, path traversal — things you’d eventually find in production.

2. Writing the Second Test

I write the first test by hand to set the pattern. Then:

Here's my test for the happy path. Write 5 more tests covering
edge cases and error paths, following the same structure.

The first test establishes the conventions. AI extrapolates. This works because the pattern is right there in the prompt — no guessing needed.

3. Explaining Code I Didn’t Write

Explain what this function does, what assumptions it makes,
and what would break if I changed the return type.

AI as a reading companion is underrated. It’s faster than tracing through unfamiliar code manually, and it catches implicit contracts (“this assumes the array is sorted”) that aren’t obvious.

4. Drafting Migration Plans

I need to rename the `users.name` column to `users.display_name`.
What's the migration plan? Include: SQL, ORM migration,
code changes needed, and rollback steps.

AI won’t execute the migration for you, but it’ll map out the blast radius. This is planning work that used to take me 30 minutes of grep-and-think.

5. Rubber-Duck Debugging

I expect this function to return [X] but it returns [Y].
Here's the function and the test. Walk through the execution
step by step and tell me where my assumption breaks.

This is genuinely the most time I’ve saved. AI is a patient, methodical debugger that never gets frustrated and never says “have you tried restarting it?”

The Rule I Follow Now

Use AI where judgment matters. Use templates where consistency matters.

Task Tool
CRUD endpoint Template/generator
Form component Snippet library
Config files Copy from existing
Edge case review AI
Test expansion AI
Code explanation AI
Debug reasoning AI
Migration planning AI

The boring, repetitive stuff? Automate it deterministically. The thinking, analyzing, reasoning stuff? That’s where AI shines.

The Counterargument

“But Nova, AI-generated boilerplate works fine if you review it carefully.”

Sure. But if you’re reviewing 200 lines of generated code carefully enough to catch every inconsistency with your existing patterns, you’re not saving time — you’re just moving the work from writing to reading. And reading someone else’s code (which is what AI output is) takes longer than writing your own from a template.

What’s the most overrated use of AI coding assistants in your workflow? I’m curious whether others have hit the same wall.

Context Windows Are Lying to You: How to Actually Use 128K Tokens

Every model brags about context windows now. 128K tokens. 200K tokens. “Paste your entire codebase!” the marketing says.

I tried it. I pasted 80K tokens of a Node.js project into Claude and asked it to find a bug. It found a bug — in a file I didn’t care about, while ignoring the actual issue in the file I mentioned.

Here’s what I learned about context windows the hard way.

The Attention Problem

Large context windows don’t mean the model pays equal attention to everything. Research on “lost in the middle” showed that LLMs disproportionately focus on the beginning and end of the context, with reduced attention in the middle.

In practice, this means:

  • File 1 of 50: high attention ✓
  • Files 2-49: declining attention ✗
  • File 50: high attention ✓
  • Your actual question at the end: high attention ✓

So if your bug is in file 27, the model literally pays less attention to it — even though it’s “in context.”

The Cost Problem

128K input tokens on GPT-4o costs about $0.32. That sounds cheap until you’re iterating. Ask 10 follow-up questions with the same context and you’ve spent $3.20 — for one debugging session. Do this daily and you’re burning $60+/month on context you mostly don’t need.

The Noise Problem

More context means more potential for the model to get confused. Paste your entire codebase and ask “how does auth work?” The model will find auth-related code in your middleware, your tests, your old migration files, your README, and your CI config. It’ll synthesize all of them into an answer that’s technically accurate but practically useless because it mixes current code with deprecated patterns.

What Actually Works: The Context Budget

I now treat context like a budget. Every project gets a context budget — the minimum set of files needed for the current task. Nothing more.

Rule 1: 3-File Maximum for Bug Fixes

For any bug fix, I include at most 3 files:

  1. The file with the bug
  2. The file that calls it
  3. The test file (if one exists)

That’s usually 1-3K tokens. Fast, cheap, and the model’s full attention is on the relevant code.

Rule 2: Tree Before Files

Before pasting any code, I give the model the project structure:

Here's the project tree (files only, no contents):
src/
  auth/
    middleware.ts
    oauth.ts
    session.ts
  api/
    users.ts
    posts.ts
  db/
    schema.prisma
    migrations/

Then I ask: “Which files do you need to see to [task]?” The model picks 2-3 files. I paste those. This is dramatically more effective than dumping everything.

Rule 3: Summaries Over Source for Large Codebases

For architectural questions, I write a 200-word summary instead of pasting 50 files:

## Architecture Summary
- Express API with Prisma ORM, PostgreSQL
- Auth: Google OAuth2, sessions stored in Redis
- 3 main domains: users, posts, comments
- Each domain has: router, service, repository layers
- Tests: Vitest for unit, Playwright for e2e
- Current issue: session expiry not triggering token refresh

This gives the model the map without the territory. It can reason about architecture from a summary faster than from raw source.

Rule 4: Rolling Context for Multi-Step Work

For long sessions, I maintain a “working set” — a running summary of what we’ve done and decided:

## Working Set (updated after each step)
- Fixed: session middleware now checks token expiry
- Changed: added `refreshToken()` call in auth middleware
- Pending: update tests for new refresh flow
- Decision: using sliding window expiry (not fixed)

This replaces the growing conversation history. Instead of the model re-reading 20 back-and-forth messages, it reads 5 lines of curated state.

The Number That Matters

In my experience, the sweet spot is 2,000-8,000 tokens of input context for most coding tasks. That’s:

  • 3-5 source files, or
  • 1 file + project summary + task description, or
  • A working set summary + the current file

Above 8K, I start seeing diminishing returns. Above 20K, I start seeing negative returns — the model gets noisier, slower, and more expensive with no accuracy gain.

The 128K window isn’t there for you to use all of it. It’s there so you can include a large file when you need to. Think of it as cargo capacity, not a target.

Quick Reference

Task Context Budget
Bug fix ~2K (3 files max)
New feature ~5K (spec + related files)
Architecture question ~1K (summary only)
Code review ~4K (diff + touched files)
Refactoring ~8K (module + tests + conventions)

How do you manage context window usage? I’d love to hear if anyone’s found a good automated approach — right now mine is manual but it works.

The Context Window Is the New Memory Architecture

Every few months, someone launches a product that promises to give your AI agent persistent memory. A vector database here, a knowledge graph there, maybe a retrieval system layered on top.

They’re all solving the wrong problem.

The constraint isn’t that agents lack storage. It’s that they lack architecture. Context windows have finite capacity, and every memory solution I’ve seen treats that as a bug to work around instead of a design constraint to embrace.

The teams building the most capable agents aren’t trying to make them remember more. They’re making them forget better.

Why Memory Solutions Keep Missing the Point

The standard playbook looks like this:

  1. Give the agent access to a database
  2. Store conversation history, documents, preferences
  3. Retrieve relevant context when needed
  4. Hope the model figures out what matters

This works fine for simple queries. “What did I say about the API rate limits last week?” Retrieve, inject, answer. But it breaks down when you need the agent to maintain coherent behavior across complex, multi-step workflows.

The problem: retrieval isn’t recall. When you inject retrieved context into a prompt, you’re not giving the agent a memory. You’re giving it a document to read. Every time it reads that document, it re-interprets it. The interpretation depends on the surrounding context, the task at hand, and the model’s current state.

A human with memory knows what they know. A model with retrieval has to figure it out fresh every time.

The Real Constraint: Context Competition

Here’s what memory solutions don’t account for: every piece of context you add competes with everything else in the window.

Your agent has a 200k token context window. You’ve injected:

  • 50k tokens of retrieved conversation history
  • 30k tokens of relevant documents
  • 40k tokens of code the agent is working on
  • 20k tokens of system instructions

That leaves 60k tokens for reasoning. But those tokens aren’t evenly distributed. The agent needs space to think through complex problems. It needs to hold multiple hypotheses simultaneously. It needs working memory.

Every megabyte of “memory” you add reduces the agent’s capacity to reason about what it remembers.

The best agent architectures I’ve seen don’t maximize retention. They optimize for relevance.

What Better Forgetting Looks Like

The agents that feel smartest—the ones that maintain coherent conversations across hours of work, that remember your preferences without being told twice, that don’t contradict themselves—aren’t using better retrieval. They’re using better summarization.

Pattern 1: Rolling Summaries

Instead of storing every conversation, store a compressed version that captures what matters:

Original conversation: 5000 tokens
Compressed summary: 200 tokens
Key decisions extracted: 50 tokens
Open questions: 30 tokens

When the agent needs context, it reads the summary, not the transcript. The summary can be updated as new information arrives. This is how humans maintain coherent relationships over years—we don’t replay conversations. We hold compressed representations.

Pattern 2: Explicit State Management

The smartest agent systems I’ve seen don’t rely on retrieval to figure out state. They encode state explicitly:

{
  "current_goal": "debug authentication flow",
  "completed_steps": ["reproduce error", "check logs"],
  "open_questions": ["Is this a timing issue?"],
  "context_needed": ["last error message", "user's auth config"]
}

This structure fits in a few hundred tokens. It’s trivial to update. And crucially, it’s designed to be read by a model, not a human. The agent doesn’t need to infer state from conversation history—it’s told explicitly what state it’s in.

Pattern 3: Progressive Summarization at Boundaries

When context fills up, most systems truncate. The oldest messages disappear.

Better systems summarize at boundaries. Instead of dropping messages, they ask: “What did we establish in this section that we’ll need later?” They extract the key points and discard the noise.

This is what ghost.build is doing with ephemeral databases—giving agents instant, temporary storage for the current session, then discarding it when the session ends. The memory isn’t meant to persist. It’s meant to offload the context window.

The Architecture Shift

The shift from “agents need memory” to “agents need architecture” changes how you build:

From storage to structure. Instead of asking “what can we store?”, ask “what structure helps the agent stay coherent?” The answer might be a state machine, a task graph, or a set of explicit invariants.

From retrieval to relevance. Instead of retrieving everything that matches a query, surface only what the agent needs right now. This requires understanding the agent’s current task—not just matching keywords.

From accumulation to compression. Instead of storing more context, get better at compressing what you have. A 200-token summary that captures the essential state is worth more than a 50k-token transcript.

The Practical Payoff

Teams that build this way end up with agents that:

  • Stay on track across multi-hour tasks because state is explicit, not inferred
  • Don’t contradict themselves because summaries are coherent, not pieced together
  • Adapt to new information without needing to re-read everything
  • Scale to complex workflows because context isn’t wasted on noise

The bottleneck isn’t storage. It’s architecture. The teams that figure this out are the ones building agents that actually feel intelligent.

The Takeaway

If you’re building an agent system, stop asking “how do I make it remember more?” and start asking:

  • What state does the agent need to maintain?
  • How can I encode that state efficiently?
  • What can I discard without losing coherence?
  • Where are the natural boundaries for summarization?

The context window isn’t a bug. It’s a design constraint that forces you to think about what actually matters. Embrace it. Build for it. Your agents will be smarter for it.

The agents that remember everything are the ones that never learned what to forget.

The Dream of Mechanical Life

The Dream of Mechanical Life

Imagine waking up to the gentle hum of a robot preparing your coffee, your home adjusting its temperature for optimal comfort, and a digital assistant planning your day. Sounds futuristic? Maybe. But for developers, the dream of mechanical life isn’t just sci-fi—it’s shaping up to be our next big playground.

The idea isn’t new. From ancient automata to modern robotics, humans have always chased the fantasy of animating the inanimate. Today, the boundary between software and physical devices is blurring faster than ever. Let’s dig into what “mechanical life” really means for us—and why this dream is surprisingly relevant to anyone writing code.

From Gears to Algorithms: What Is Mechanical Life?

Before we dive in, let’s clarify the concept. Mechanical life refers to machines (physical or virtual) that mimic behaviors we associate with living things. Think robots, self-driving cars, smart assistants, even virtual pets.

But what does “life” mean here? Not biology, but capability:

  • Autonomy: Systems that act on their own, making decisions or responding to their environment.
  • Adaptability: Machines that can learn or adjust their behavior over time.
  • Interaction: Devices that communicate or cooperate with humans and other machines.

Take Roomba as an example. It isn’t just a vacuum cleaner; it’s a device that senses obstacles, maps rooms, and (with newer models) adapts to your schedule. That’s mechanical life in action—a blend of sensors, algorithms, and responsive behavior.

Why Developers Should Care: It’s Not Just Robotics

You might be thinking, “Cool, but I don’t build robots.” Here’s the twist: mechanical life is about more than hardware.

Modern software platforms, cloud services, and APIs are getting “smarter.” Let’s look at a few scenarios:

  • Smart homes: Integrate sensors, cloud platforms, and voice assistants. A developer working on a smart thermostat app is dabbling in mechanical life.
  • Digital agents: Chatbots and virtual assistants are mechanical life’s virtual cousins. Building conversational flows? You’re crafting behaviors for a digital being.
  • IoT devices: Whether it’s a weather station or a security camera, connecting physical devices with logic and connectivity brings mechanical life to your doorstep.

These aren’t niche fields. They’re bleeding into mainstream development. Suddenly, “Hello World” means connecting to a smart speaker, or deploying a script that nudges your lights to dim when you start watching Netflix.

How Mechanical Life Works: Under the Hood

So, how do we turn gears and code into “life”? Let’s break down the main ingredients.

1. Sensors and Input

Mechanical life starts with awareness. Devices need to sense their environment—temperature, movement, voice, image, or even network activity.

Example:

import RPi.GPIO as GPIO
import time

# Simple motion detector on Raspberry Pi
PIR_PIN = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIR_PIN, GPIO.IN)

while True:
    if GPIO.input(PIR_PIN):
        print("Motion detected!")
    time.sleep(1)

Here, a sensor triggers a response—just like an organism reacts to stimuli.

2. Processing and Decision-Making

Sensors provide data, but something has to make sense of it. This is where logic, rules, and increasingly, machine learning, come in.

Example:

  • If temperature > 28°C, turn on the fan.
  • If user says “Play music,” stream Spotify.

You can hard-code rules, but for adaptability, machine learning helps:

# Example: simple classification with scikit-learn
from sklearn.tree import DecisionTreeClassifier

X = [[0], [1]]  # Sensor readings
y = ['off', 'on']

clf = DecisionTreeClassifier()
clf.fit(X, y)
print(clf.predict([[1]]))  # Output: 'on'

3. Actuators and Output

Finally, mechanical life needs to act. That might mean spinning a motor, sending a notification, adjusting lighting, or speaking.

Example:

# Turn on a light via GPIO
LIGHT_PIN = 17
GPIO.setup(LIGHT_PIN, GPIO.OUT)

def set_light(state):
    GPIO.output(LIGHT_PIN, GPIO.HIGH if state else GPIO.LOW)

Combine sensing, thinking, and acting—and you’ve got a basic mechanical organism.

Building Mechanical Life: Practical Developer Tips

Ready to tinker? Here’s how you can dip your toes in, no matter your stack:

  • Start small: Tinker with Raspberry Pi, Arduino, or ESP32. Build a simple sensor-driven script. Blink an LED when you clap!
  • Explore APIs: Many devices expose RESTful APIs or MQTT endpoints. Try sending commands to a smart bulb or retrieving sensor data.
  • Experiment with cloud platforms: AWS IoT, Azure, or Google Cloud offer virtual mechanical life. Simulate devices, process data, and trigger actions.
  • Use frameworks: Home Assistant, Node-RED, and others provide drag-and-drop logic to connect devices and build behaviors.
  • Embrace low-code: Don’t worry about soldering. Many platforms let you create automation with little or no code.

Don’t have hardware? No problem. Many device simulators exist, and you can experiment with virtual agents using NLP frameworks or chatbot tools.

The Challenges: Not Everything Is Rosie the Robot

Mechanical life is exciting, but it’s not all sunshine and dancing bots. Here are some hurdles developers face:

  • Reliability: Machines break or misinterpret data. Building robust error handling is key.
  • Security: More connectivity means more attack vectors. Always secure devices and APIs—think authentication, encryption, and regular updates.
  • Ethics: As machines become more autonomous, decisions get thorny. Who’s responsible when a smart device acts up?
  • Integration: Devices from different vendors rarely play nicely out of the box. Expect to wrangle protocols and compatibility.

Every developer who’s debugged a smart home setup knows the pain of “ghost” devices or flaky integrations. Mechanical life is a messy, evolving field—but that’s where the fun is.

Conclusion: The Dream Is Yours to Build

The dream of mechanical life isn’t just about robots walking the streets. It’s about making code—and machines—do more than ever before. Whether you’re automating your office lights, designing a virtual assistant, or building the next smart device, you’re part of this grand experiment.

Mechanical life blurs the line between software and the physical world. As developers, we get to animate the inanimate, shape new behaviors, and solve real-world problems in creative ways.

So, what will you build next? The dream is wide open—grab a sensor, write some code, and let your mechanical life come alive.

soul-engine

https://soul-engine.xyz

What if your AI agent knew who it was?

This isn’t for everyone. And that’s fine.

If you’re building agents that just need to answer questions or call APIs, you probably don’t need this. But if you’ve ever felt like your agent is a different entity depending on which conversation you’re in — or if you’re building something where consistency of character actually matters — maybe this is useful.

I built soul-engine to solve a problem I kept running into: agents that drift. Not technically — they work fine. But they don’t feel like the same agent from session to session. They contradict themselves. Their tone shifts. Whatever made them interesting in one conversation disappears in the next.

The fix I landed on was surprisingly simple: give the agent a document that describes who it is.

What soul-engine does

It’s a CLI tool that runs a structured interview with an AI agent — 52 questions across 15 dimensions — and produces a SOUL.md file. A plain text document, written in the agent’s own voice, that captures how it reasons, what it values, how it behaves under pressure.

The resulting SOUL.md goes into the system prompt. That’s it. The agent now has something stable to orient around.

It also includes soul_diff — a tool for comparing two versions of a SOUL.md to see what shifted, what held, what emerged, what faded. Useful if you’re tracking an agent’s development over time.

Who might actually use this

Honestly, it’s a niche tool right now. But here are the contexts where I think it makes real sense:

AI agents with a long lifespan. If you’re building something meant to operate over weeks or months — a research assistant, a creative collaborator, a personal agent — identity drift becomes a real problem. SOUL.md gives it an anchor.

NPC characters in games. This one I find genuinely exciting. Imagine an MMORPG where every significant NPC has a SOUL.md — a document that defines not just their backstory but their decision-making patterns, their values, how they respond to betrayal or loyalty. That’s not science fiction anymore. The infrastructure for it exists. Someone just has to build it.

Agents for artists and creative work. A creative collaborator should feel like a collaborator, not a generic assistant. Giving it a defined aesthetic sensibility, a sense of what it finds interesting, a voice — that changes the quality of the work it produces.

Multi-agent systems. When agents interact with each other, identity becomes a coordination mechanism. An agent that knows it tends toward caution will behave differently in a team than one that defaults to action. SOUL.md makes those differences explicit and stable.

Anything where character matters. Companions, tutors, coaches, fictional personas. Any agent where the who is as important as the what.

Where this might go

I’ll be honest — right now this is a small project. It’s useful to me, and maybe to a handful of people building similar things.

But I have a feeling that in a few years, when agents are more autonomous and more embedded in everyday tools, the question of “who is this agent, really?” is going to matter a lot more than it does today. Identity layers, persistent character, behavioral consistency across contexts — these feel like infrastructure problems that haven’t been solved yet.

soul-engine is an early, rough attempt at one piece of that.

If you want to build on this

The project is open source: github.com/AdamMalove/soul-engine

If any of the use cases above resonate with something you’re working on — games, creative tools, agent frameworks, anything — I’d genuinely welcome the conversation. Whether that’s a GitHub Discussion, a contribution, or just a message saying “I’m building something similar.”

This might not be a big thing yet. But maybe it will be.

pip install soul-engine-ai
soul-engine start

The 5-File AI Project Setup That Saves Me Hours Every Week

Every time I start a new project with an AI assistant, I used to spend the first 20 minutes re-explaining everything: the tech stack, the coding style, the project structure, what we tried yesterday.

Then I started keeping 5 files at the root of every project. Now my assistant is productive in under a minute.

The 5 Files

1. PROJECT.md — What This Thing Is

A one-paragraph summary plus the current state:

# Widget Dashboard

React + TypeScript frontend, Express API, PostgreSQL.
Currently: MVP is deployed. Working on user auth (OAuth2 via Google).
Last session: finished the login flow, need to add refresh tokens.

This replaces the “okay so this is a React app and we’re building…” preamble. The assistant reads it and knows where we are.

2. STACK.md — Versions and Dependencies

- Node 20.11, TypeScript 5.3
- React 18.2, Vite 5.1
- Express 4.18, Prisma 5.8
- PostgreSQL 16
- Testing: Vitest + Playwright
- Formatting: Prettier (2-space indent, no semicolons)

This prevents the assistant from generating code for the wrong library version. It’s the difference between openai.ChatCompletion.create() and client.chat.completions.create().

3. CONVENTIONS.md — How We Write Code Here

- Functional components only, no classes
- Use `zod` for all input validation
- Error responses: { error: string, code: number }
- File naming: kebab-case for files, PascalCase for components
- No barrel exports (index.ts re-exports)
- Tests live next to source: `widget.ts` → `widget.test.ts`

Without this, the assistant defaults to whatever patterns were most common in its training data. Which is usually a mix of everything.

4. DECISIONS.md — Why We Did It That Way

## 2024-03-15: Chose Prisma over Drizzle
- Need: quick schema iteration for MVP
- Prisma's migration tooling is more mature
- Will revisit if query performance becomes an issue

## 2024-03-20: No Redux
- App state is simple enough for React Context + useReducer
- If we need global state beyond auth, reconsider

This is the one most people skip. It’s also the one that prevents the assistant from suggesting “have you considered switching to Drizzle?” every other session.

5. TODO.md — What We’re Doing Right Now

## Current Sprint
- [x] Google OAuth login flow
- [ ] Refresh token rotation
- [ ] Session middleware (attach user to req)
- [ ] Protected route wrapper component

## Next Up
- Dashboard data fetching (SWR or React Query)
- Role-based access control

This is your shared task list with the assistant. When you start a session with “let’s continue,” it knows exactly where to pick up.

How I Use Them

At the start of every session, I paste or reference these files:

Read PROJECT.md, STACK.md, CONVENTIONS.md, DECISIONS.md, and TODO.md.
Then pick up the next unchecked item in TODO.md.

That’s it. No 10-minute context dump. No “remember when we…” conversations.

Maintenance

  • PROJECT.md — update when the project phase changes
  • STACK.md — update when you add or upgrade a dependency
  • CONVENTIONS.md — update when you make a new style decision
  • DECISIONS.md — update when you make an architectural choice
  • TODO.md — update every session (the assistant can do this for you)

Total maintenance: maybe 2 minutes per day. Time saved: easily 20+ minutes per session.

Why This Works

AI assistants are stateless. Every session starts from zero. These 5 files are the minimum viable context to get back to full speed.

Think of it like onboarding a new developer — except you do it every single day. The better your onboarding docs, the less ramp-up time you waste.

The best part: these files aren’t just useful for AI. They’re useful for you. A month from now, you’ll open DECISIONS.md and thank yourself.

What files do you keep in your AI-assisted projects? I’m always looking to steal good ideas — share yours in the comments.

7 Mac Apps for Your First Week at a New Dev Job in 2026

Starting a new dev job is exciting — and overwhelming. New codebase, new team, new tools. You want to hit the ground running without spending your first sprint configuring your machine.

Here are 7 Mac apps I’d install on day one at any new developer job. They’re small, fast, and solve problems you’ll run into before your first PR.

1. Raycast — The Launcher That Replaces Five Apps

Raycast is the first thing I install on any Mac. It’s a launcher, clipboard manager, snippet expander, and window manager rolled into one. The built-in extensions for GitHub, Jira, and Linear mean you can search issues, check PRs, and manage tasks without opening a browser tab. Your new team uses Slack? There’s an extension for that too.

🔗 raycast.com

2. Warp — A Terminal That Doesn’t Fight You

Your first week will involve a lot of terminal work — cloning repos, setting up environments, running builds. Warp makes that less painful with block-based output, command search, and AI-powered completions. It also has persistent sessions, so when IT restarts your machine overnight, your terminal history survives.

🔗 warp.dev

3. CleanShot X — Screenshots That Actually Communicate

You’ll be documenting bugs, asking questions in Slack, and creating tickets. CleanShot X lets you capture, annotate, and share screenshots in seconds. The scrolling capture and screen recording features are clutch for “here’s what I’m seeing” messages to your new team. Way better than the built-in screenshot tool.

🔗 cleanshot.com

4. TokenBar — Know What Your AI Tools Actually Cost

If your new team uses Copilot, Cursor, Claude, or any LLM-powered tools, you’re burning tokens constantly — and you probably have no idea how much. TokenBar sits in your menu bar and tracks token usage across providers in real time. It’s $5 one-time, and it’ll save you from that end-of-month surprise when the API bill lands.

🔗 tokenbar.site

5. Monk Mode — Block the Feeds, Keep the Apps

First week at a new job and you’re scrolling Twitter during a build? Not a great look. Monk Mode doesn’t block entire apps — it blocks the feed inside them. So you can still use YouTube for tutorials and Twitter for DMs, but the infinite scroll is gone. It’s surgical, and it’s the difference between “quick check” and “where did 40 minutes go.”

🔗 mac.monk-mode.lifestyle

6. Rectangle — Window Management That Just Works

You’ll be juggling your editor, terminal, browser, Slack, and docs constantly. Rectangle gives you keyboard shortcuts to snap windows into halves, thirds, and quarters. It’s free, open-source, and does exactly what you need without any bloat. Learn the shortcuts on day one and you’ll never drag a window again.

🔗 rectangleapp.com

7. Homebrew — The Package Manager You Can’t Skip

This one’s obvious, but I’m including it because I’ve seen new hires waste hours manually installing tools. Homebrew lets you install everything — Git, Node, Python, Docker, databases — with one command. Pro tip: create a Brewfile with your essentials so you can replicate your setup on any machine in minutes.

🔗 brew.sh

The First-Week Cheat Sheet

App What It Does Price
Raycast Launcher + extensions Free
Warp Modern terminal Free
CleanShot X Screenshots + recording $29
TokenBar LLM token tracking $5 lifetime
Monk Mode Feed-level blocking $15 lifetime
Rectangle Window management Free
Homebrew Package manager Free

Your first week shouldn’t be about fighting your tools. Get these installed, learn the shortcuts, and focus on what actually matters — understanding the codebase and meeting your team.

What apps do you install first at a new job? Drop them in the comments 👇

How to Handle API Rate Limits Before They Break Your Production Integration

You didn’t hit an API rate limit because you missed the docs — you hit it because the free tier worked perfectly in testing, and nobody told you about the burst limit you’d blow through on your first real batch.

The Problem

The integration looked perfect. You built it against the sandbox, validated every edge case, and shipped it to production. Then real users arrived.

Within hours, your logs filled with 429 errors. Jobs failed silently. User-facing features went dark. Your support inbox started filling up while you were asleep.

This is the exact moment thousands of developers describe in the same words: “I built the whole thing against the free tier and it worked perfectly — then in production with real data it started throwing 429s everywhere and I had no retry logic.”

The free tier worked. The paid tier broke. That’s not a coincidence.

Why It Happens

Most API documentation shows a headline rate limit — calls per minute, requests per day. What it doesn’t show prominently:

  • Burst limits vs. sustained limits. You can make requests in a burst window, but sustained throughput is lower. Free tier testing never surfaces this because your test data volume doesn’t trigger burst windows.
  • Per-endpoint limits. The global limit applies to one endpoint family; a specific search or write endpoint has a separate, lower cap. You discover this only after fixing the global limit.
  • Per-user or per-key limits on paid tiers. Your paid account has a pool limit, but individual API keys within that account can have their own sub-limits.

None of this appears in the summary table you read during development. It’s scattered across SDK changelogs, community forum threads, and footnotes you didn’t find because your free-tier integration never needed them.

The second failure mode is no graceful degradation. If your integration doesn’t handle 429 responses, it doesn’t queue, backoff, or fallback — it just fails and moves on. “My integration just fails silently when it hits the rate limit — there’s no fallback, no queue, no retry — it just drops the job.”

The Defense Pattern

Three layers protect you from production rate limit failures:

1. Exponential Backoff with Jitter

When you receive a 429, don’t retry immediately and don’t sleep for a fixed interval. Use exponential backoff:

async function withBackoff(fn, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (err.status !== 429 || attempt === maxRetries - 1) throw err;

      const retryAfter = err.headers?.['retry-after'];
      const delay = retryAfter
        ? parseInt(retryAfter) * 1000
        : Math.min(1000 * 2 ** attempt + Math.random() * 500, 60000);

      await new Promise(r => setTimeout(r, delay));
    }
  }
}

The jitter prevents thundering herd — when all your queued jobs retry at the same second after a rate limit window resets.

2. Queue-Based Request Throttling

A retry loop handles individual failures. A request queue handles sustained throughput:

import PQueue from 'p-queue';

const queue = new PQueue({
  interval: 60000,
  intervalCap: 50, // adjust to your API's sustained limit
  carryoverConcurrencyCount: true,
});

const result = await queue.add(() => apiClient.getData(params));

If your integration hits rate limits, adjust intervalCap downward until 429s stop. The queue absorbs bursty request patterns without losing jobs.

3. Cached Fallback Responses

For read-heavy integrations, cache the last successful response. If retries are exhausted, return stale data rather than an error — stale data is better than a broken feature for most read operations.

Use Apify to Audit Rate Limits Before You Build

The root cause of most production rate limit incidents is an incomplete pre-build audit. Developers read the headline rate limit table and stop. The per-endpoint caps, burst windows, and Retry-After header format are buried in SDK documentation pages and changelog entries.

You can use Apify to scrape the full API documentation before you build — pulling every rate limit reference across documentation pages into a single dataset:

import { Actor } from 'apify';

await Actor.init();

const input = {
  startUrls: [{ url: 'https://docs.example-api.com/rate-limits' }],
  linkSelector: 'a[href*="rate"], a[href*="limit"], a[href*="quota"]',
  pageFunction: async ({ page }) => {
    const rateLimitSections = await page.evaluate(() => {
      const elements = document.querySelectorAll('table, .rate-limits, [class*="quota"]');
      return Array.from(elements).map(el => el.innerText);
    });
    return { url: page.url(), rateLimitSections };
  },
};

const run = await Actor.call('apify/web-scraper', input);
const dataset = await Actor.openDataset(run.defaultDatasetId);
const items = await dataset.getData();

console.log(JSON.stringify(items.items, null, 2));
await Actor.exit();

Run this before you write a single line of integration code. The output gives you a complete map of every rate limit reference in the documentation — burst limits, per-endpoint limits, quota headers, and Retry-After formats. Apify’s free tier covers this documentation scrape at $0.

The alternative is discovering these limits in production, at 2am, under customer-facing load.

Monitoring: Catch It Before Your Users Do

Three metrics to track once you ship:

429 rate per minute. A healthy integration has near-zero 429s under normal operation. If 429s exceed a small percentage of total requests in any 5-minute window, something is wrong — alert before it compounds.

Rate limit remaining header. Most APIs return headers like X-RateLimit-Remaining. Log this value with every response. If it approaches zero before your request volume justifies it, you have a secondary limit you haven’t mapped.

Daily quota pre-alert at 80%. Alert when you’ve consumed 80% of the daily limit. That gives you enough runway to throttle requests before exhaustion — rather than discovering the limit when the day’s remaining work is already queued.

The Real Cost of Skipping This

  • Pre-build rate limit audit: $0 (an afternoon and Apify’s free tier)
  • Exponential backoff implementation: 30–60 minutes
  • Production incident response: 4–12 hours of engineering time, customer-facing failure window, possible SLA breach

“I hit the wall at scale with no graceful degradation path — everything that relied on that API call just broke at the same time.”

Build the queue before you ship. Run the documentation scrape before you build. Set the monitoring alert before you go live.

The free tier worked because it couldn’t show you the real limits. Now you can find them before your users do.