SIDE PROJECT · CURRENT
Penny
One operating surface for messages, research, scheduling, markets, publishing, and the services behind them.
SCALE
304 commands across 56 domains, exposed through one surface.
IMPACT
It replaced a fleet of five hundred agents with one, and has run every day since October 2025.
BYLINE
It posts under its own byline, with a published body of work.
CONTROL
Operations with real consequences are checked in code and policy.
THE REASONING
Why one agent, not a fleet
OPTIONS CONSIDERED
-
Rejected
Fleet of 500 specialized micro-agents: Shared scheduler coordinating narrow single-purpose agents. Built and ran in production, producing fragmented state and compounding coordination overhead.
-
Rejected
Hierarchical supervisor agent: Meta-agent layer auditing and orchestrating worker agents. Added meta-communication complexity without improving output quality.
-
Chosen
Single agent with broad tool capability: One careful agent running sequentially in an inspectable loop across stable CLI tools and deterministic exit codes.
DECISION & REASONING
- A year ago this ran a fleet of five hundred agents. I tore it down.
- Every fleet version produced the same mountain: half-finished features, quietly buggy work, pieces that did not fit together.
- Supervising agents added coordination, not quality.
- One careful agent on real platforms holds together. Anthropic and Cognition have since published the same advice.
Why no fallback chain
OPTIONS CONSIDERED
-
Rejected
Cost-aware fallback cascade: Automatic failover to cheaper or faster models on error. Shipped confident hallucinations and degraded output silently when upstream failed.
-
Rejected
Dynamic capability router: Heuristic routing layer picking models per prompt. Obscured model behavior and added failure surface before execution began.
-
Chosen
Named single runner that fails loud: Explicit model selection per task. Fails immediately and visibly on error so humans decide whether to retry, adjust, or stop.
DECISION & REASONING
- A chain that quietly retries with a weaker model was how I shipped confident answers nobody asked for.
- If the runner fails, I want to see it fail and decide.
- Penny launched without one.
Why n8n, not my own engine
OPTIONS CONSIDERED
-
Rejected
In-chat turn execution: Handling stateful loops, retries, and timers directly inside agent turns. Brittle across restarts and couples long-running jobs to chat lifecycles.
-
Rejected
Bespoke custom workflow engine: Homegrown job queue and state machine. Ran reliably for a year but created constant maintenance overhead for plumbing instead of capabilities.
-
Chosen
n8n in Redis queue mode: Battle-tested orchestration engine running workflows as thin coordinators calling deterministic CLI commands, with visual state and built-in retries.
DECISION & REASONING
- Durable, scheduled, branching work has no business living inside a chat turn.
- Maintaining my own engine was a second job.
- I retired a working system I was fond of because the boring one was better.
Why the gate is an exit code
OPTIONS CONSIDERED
-
Rejected
Prompt discipline guardrails: Instructing the model in system instructions to ask before acting. Prompts degrade under context pressure, jailbreaks, and tool hallucination.
-
Rejected
Per-workflow destination allowlists: Configuring authorized actions per workflow trigger. Fragmented security policy across 56 domains with high configuration drift.
-
Chosen
Structural approval token gating (Exit 3): The CLI wrapper enforces security deterministically. Irreversible actions require an explicit token or the process halts with exit code 3.
DECISION & REASONING
- I do not trust a prompt to enforce a rule.
- The gate is in the exit code. It runs on every execution and never retires.
- Direct sends and irreversible actions require a human token.
- Narrowly scoped scheduled lanes carry their own explicit token.
Why messages buffer until go
OPTIONS CONSIDERED
-
Rejected
Immediate per-message execution: Invoking the agent on every inbound chat bubble. Triggers race conditions, duplicate runs, and premature actions during burst texting.
-
Rejected
Debounce timeout heuristic: Waiting 15 to 30 seconds of inactivity before firing. Adds unnecessary latency when ready, interrupts when composing longer multi-message thoughts.
-
Chosen
Deterministic buffer flushed by explicit ‘go’: Inbound messages accumulate in a Redis list until an explicit command flushes the buffer as a single coherent prompt.
DECISION & REASONING
- Because I text in bursts.
- The buffer lets me send three messages, change my mind, and finish the thought before Penny acts on any of it.
- A timer guesses when I’m done.
goknows.
Why every integration is one command
OPTIONS CONSIDERED
-
Rejected
Bespoke per-service agent tool bindings: Direct TypeScript or Python SDK integrations defined inside the agent loop. Brittle schemas, poor testability outside LLM sessions.
-
Rejected
Split surfaces for agent and workflows: Separate API clients for n8n workflows and chat agent. Duplicated authorization logic, diverging behaviors, and double the audit burden.
-
Chosen
Unified CLI surface (
pen <domain> <action>): Single binary contract called identically by the agent, n8n workflows, and human operator from terminal. Centralized logging, exit codes, and auth.
DECISION & REASONING
- 56 domains is a lot of integrations for one person to keep working. Each one gets the same shape or none of them stay working.
- One surface to write, one place to audit, one place to rate-limit.
- Three callers: the agent from inside its loop, the workflows from outside it, me from a terminal.
- Nothing is bespoke per caller, and the exit code means the same thing to all three.
Durable outbox and container restart sequencing · pending integration
INCIDENT & RESOLUTION
- Penny’s replies are delivered by a container it is also permitted to restart.
- During one execution, the agent restarted that delivery container mid-run. The workload completed, but the outbound confirmation was dropped when the container halted.
- The immediate mitigation sequences container restarts last.
- The permanent resolution implements a durable outbox queue, currently staged in
_pending-integration.
Approval boundary audit: pen CLI wrapper vs raw HTTP · open limitation
BOUNDARY AUDIT & LIMITATION
$ pen voice call --to +1555... --purpose "smoke test" {"ok":false, "error":"approval_required"}
- The
penwrapper returns exit code 3 on unapproved external writes, halting execution across all commands invoked throughpen. - However, direct HTTP requests via curl or n8n HTTP Request nodes bypass the wrapper and avoid exit-code evaluation.
- Because Penny generates automation workflows directly, unvetted workflows could theoretically execute outbound calls without wrapper gating.
- Comprehensive enforcement requires egress packet inspection at the container network boundary.