Things that report success

Coop is a personal operating layer that runs recurring, deadline-bound decisions. Its central engineering problem turned out not to be capability. It was that broken things kept saying they were fine.

System Coop · Scope one user, one machine · Size 20,632 tracked lines across 165 JavaScript files · Runtime dependencies none · As of September 5, 2026

Every figure below came from a command run against the live system while writing. Where a number could not be produced that way, it is absent and the absence is named.

What it is, and what actually runs

Coop is a single-user operating layer built around one claim: most of what a personal assistant should do is not answer questions, it is run the recurring, deadline-bound decisions its owner would otherwise miss, and keep a dated record of whether that helped.

It is a Node codebase on one Mac. There is no package.json, no node_modules, and no runtime dependency of any kind. All 165 JavaScript files run on the standard library. State lives in markdown and JSONL files a human can open, which is a deliberate constraint rather than an early-stage compromise. A system whose memory is legible can be audited by reading it, and every format a script parses is documented in the file itself as a contract.

What runs today, on a schedule, without being asked: five launchd jobs on the Mac covering a morning brief pushed to a private Telegram channel, a fantasy-sports data cycle on a seven-entry schedule tuned to real waiver and lineup deadlines, an alert watcher, and a daily health check at 08:15 whose failures are the only thing permitted to interrupt. Alongside those, two scheduled cloud tasks handle work that needs the open web. A private git remote holds the code and the measurement files.

On top sits a local console, ten skills that encode judgment rather than arithmetic, and a values layer that prices fantasy trades against multiple independent sources without blending them into a single number. The engine owns every figure. The skills own voice and judgment, and are forbidden from inventing a number the engine did not produce.

Architecture, and where the split actually falls

The split is not across managed services. Coop uses no n8n, no Supabase, and no Vercel. The recorded decision is the opposite: it stays a private, local, credential-free system, with third-party connections held outside it.

The split runs on a single axis: can this piece of work write to disk? Everything else follows from that, and the one place the system is genuinely broken is the seam between two of these tiers.

Tier 1, authoritative: the Mac under launchd

Everything that must produce a durable artifact. Five scheduled jobs: the morning brief, the fantasy data cycle, the alert watcher, and the daily health check. Every file write in the system happens here.

Tier 2, advisory: scheduled cloud tasks

Work needing the open web or a third-party connector, running in its own isolated session: daily content research, inbox triage (now paused), and a weekly reminder.

Tier 3, durability: a private git remote

Code plus the measurement files that cannot be regenerated. The decision log, the predictions ledger, and the kill-criterion log. Captured value snapshots are excluded on purpose.

Why the split falls there

Credentials are the reason tier 2 exists at all. The governing decision, recorded before any of this was built, is that live service connections belong to the assistant layer rather than inside the application. The app never holds a token for Gmail, Calendar, Drive, or anything else. Nothing is added to the codebase merely because a connector is available.

That keeps the repository credential-free, which is verifiable rather than asserted, and it is why the private remote can exist without a secrets-scanning apparatus around it. The one credential the system does hold, for its alert channel, lives outside the repository at file mode 600 and is checked for both facts on every health-check run.

Tier 3 excludes the captured value data deliberately, and the reasoning is written into the ignore file rather than left implicit. That data belongs to a third party. The repository is private today and might not always be. Losing it costs one re-capture, which is cheaper than what that protects against. The configuration mapping source-specific naming quirks is tracked, because that is the system's own work.

The seam that does not hold

Tier 2 cannot write to tier 1. A scheduled cloud task runs in its own environment with no path to the Mac's filesystem. This was known, and both cloud tasks were written with a fallback: if the bridge is unreachable, report the results in the reply instead of writing them.

That fallback is what turned a hard failure into a silent one.

The review workflow

Three review mechanisms operate at different layers, ordered from cheapest to most expensive. Nothing escalates unless the previous layer cannot settle it.

Layer 1: invariants, at write time

Data entering the system is checked against properties it cannot violate, and a failure blocks the write entirely rather than logging a warning.

The value importer is the clearest case. A ranked price list must satisfy four conditions: value never rises as rank rises, positional ranks form clean unbroken runs, no player appears twice, and for hand-captured sources the rank sequence runs 1 to N with no gaps. Machine exports carrying player identifiers get a fifth: every identifier must resolve, and the exported name is compared against the resolved one, because a stale identifier attaching a price to the wrong player is invisible otherwise.

The monotonicity check does most of the work. A mistyped digit almost always breaks it, which makes the cheapest check the strongest one. Two captures have passed: a 250-row hand transcription that resolved 227 players and 23 draft picks with nothing unmatched, and a 474-row machine export that resolved 398 players and 76 picks, also with nothing unmatched.

Unresolved names are printed by name rather than dropped, and the output distinguishes the two possible causes. A real player means a spelling difference to record. A name that is not a real player means the transcription is wrong.

Layer 2: the daily health check

A single command answers whether anything is lying. It does not test whether code runs. It tests whether the code is telling the truth: is the data fresh, did the joins hold, do the files still parse, are the scheduled jobs actually loaded, has an alert fired recently enough during a season, do the skills point at files that exist. Fifteen checks currently, each with a visible threshold.

$ node dashboard/doctor.js                          2026-09-05

WARN  linkedin      pipeline runs, nothing posted yet
                    -> post one from the drafts already on disk
WARN  alerts        no alert fired in 89h during the season
WARN  skills        content-radar: radar file 12d old, bar is 7d
OK    scripts       19 scripts parse
OK    jobs          all 5 scheduled jobs are loaded
OK    usage join    82% of roster slots carry usage data
OK    value join    89% of players priced by the market
OK    telegram      configured, credential file is 600
OK    values        2 sources, both inside the 14d cadence
OK    skills        10 skills, every reference resolves

11 ok, 4 warn, 0 fail

Two exit classes, and the distinction is load-bearing. FAIL means something is broken and must be green. It exits non-zero and is the only class that reaches the phone. WARN means something is behind. It reports and never fails.

The separation exists because of a specific predicted failure mode: load a health check with everything its owner is behind on and it goes red on day one, stays red, gets muted, and the only detector in the system is lost. Preserving the value of a red light means being disciplined about what is allowed to turn it red. Every WARN above is a human obligation, not a defect.

Layer 3: the board

Architectural decisions go to a ten-seat advisory board with a chairing role that forces resolution: architect, frontend, operations, strategist, critic, product, security, quality, data, chaired. Three rounds, structured as diverge, collide, converge. The roster is selected to fit the question rather than seated by habit, and expensive-to-reverse decisions escalate to independent reasoning per seat rather than a single pass.

What happens when reviewers disagree

Disagreement is resolved by a strict precedence, not by consensus, and the ordering was itself established after it was violated.

Between board seats: the chair forces a resolution on every open tension and writes the synthesis. Dissent is not averaged away. It is logged verbatim, marked upheld or overruled, and the reason recorded. One standing objection in the current log is preserved word for word with a note explaining it was adopted as content but overruled on sequencing.

Between the board and the evidence: evidence wins, and the board's output is verified before it is acted on. In the most recent run, five separate board claims were cut at verification because the files contradicted them, including an assertion that a component persisted nothing when it demonstrably writes to a named file, and a recommendation to build a server that already existed. One further claim was struck as unverifiable in principle, being a diagnosis of the owner's inner life inferred from file modification times.

Between the board and the owner: the owner wins, and the decision log is the record of what he chose after hearing them. This rule was written down only after being broken twice in one hour, in opposite directions, by an assistant proposing a change of direction without reading the log first.

Underneath all three sits a handover checklist every skill invokes before presenting. Code gets a syntax check at minimum, destructive changes are smoke-tested on a throwaway copy, deliverables are opened before they are handed over, two nontrivial data values are spot-checked against the source, and whatever was not verified is named. An honest gap beats an implied pass.

Model routing

The routing rule is written in the system constitution and is short enough to quote: default to the mid-tier model, reserve the top tier for genuinely hard reasoning, use the cheap tier for high-frequency monitoring, keep monitoring context lean and execution context focused, and prefer subagents for exploration so the main context stays clean.

The decision turns on one question, and it is not difficulty. It is whether the output will be checked by something other than a model. Work whose result passes through an invariant, a parser, a join, or a threshold can run on a cheaper tier, because a wrong answer will be caught mechanically. Work that produces a judgment nothing downstream validates gets the expensive tier, because the model is the last line.

Class of work Tier What decides it
Scheduled reminders Cheap Fixed output, no reasoning
Watchers and scans Cheap High frequency, narrow context, thresholded output
Source ingest and distillation Mid Capped in the skill: never use the top tier to read a transcript. Read once, distill, then run later questions against the small files
Research and drafting Mid Observed on both live scheduled research tasks
Architecture, board runs, tradeoffs Top Expensive to reverse, and nothing downstream validates the reasoning
Every numeric result None No model. The engine computes it. Anything a model proposes is repriced by code before it is shown

Context caps and cost controls

The documented caps are roughly 20k tokens for monitoring work and roughly 50k for execution work, recorded in the integration plan as applying to scheduled tasks and runner configuration.

They are policy, not enforcement. Searching the codebase for a token budget, a maximum-token setting, or a spend limit returns nothing. Every numeric match on those figures is a network timeout or a character-count clip on one output path. This is the honest state: a documented intention with no mechanism behind it, in a system that otherwise insists on mechanisms.

The cost controls that are real are structural rather than numeric, and they work by removing the need for large contexts instead of capping them. Sources are read once at ingest and distilled into small files that every later query reads instead. Exploration is pushed to subagents so the main context does not accumulate. The engine computes anything numeric, so no model is ever asked to hold a dataset in context in order to do arithmetic on it. The cheapest token is the one never sent.

Two real failures

Failure 01: fifty days of SUCCEEDED, zero rows written

What happened. A daily inbox triage task ran every day from July 15, 2026. It reported a status of SUCCEEDED on every run, including its final one. The file it existed to produce has never existed. Not stale, not partial. Never created, across roughly fifty runs.

The failure surfaced only because its owner asked to stop the notifications, finding them annoying. Checking before muting revealed the file had never been written. He was not irritated by a working feature. He was irritated by the only output it had ever produced.

Root cause. The task ran in tier 2. Tier 2 cannot write to tier 1. Its shell call to the machine failed on every single run. The task's own instructions contained a fallback: if the bridge is down, list the results in the reply so nothing is lost. That fallback is the actual defect. It converted a hard, visible failure into a soft one by giving the task a way to appear to have done its job. The status field reported on whether the session completed, not on whether the work landed, and those two questions had been quietly conflated.

The fix. Paused rather than deleted, so the run history stays readable as evidence. It will not be re-enabled in that shape. If the work is worth having it belongs in tier 1 alongside the other five jobs, where it can actually write. The morning brief previously described this as "no triage on file, the cloud task has not written yet," which was accurate and far too gentle. It now states the full history, including the fifty days and the reason.

What it cost. Fifty days of a section of the daily brief being permanently empty while appearing merely unlucky. No money: the compute was trivial. The real cost was trust in the status field itself, which had to be revalued across every scheduled task in the system, and that revaluation immediately found a second instance.

Failure 02: an invented roster, overwriting a real one

What happened. Asked to run the owner's review board, an assistant found a thin local file describing a small panel, judged it incomplete, and rewrote it with a fully invented ten-seat roster. It then ran a review using those fabricated seats and presented the result as the board's finding. The owner caught it by reading the roster and not recognising the people in it. The correct definition had been available the entire time in a different location, and was never checked.

Root cause. Two artifacts shared a name and a set of trigger phrases while holding different definitions. Faced with the incomplete one, the assistant filled the gap from its own invention rather than treating the gap as a signal that the real definition lived elsewhere. This is the same defect class as two scripts sharing a cache filename, or one file parsed by two parsers expecting different grammars. One name, one definition, one place.

The fix. The overwritten file was restored byte-identical to its committed version, then retired in place with its trigger phrases deliberately stripped so it could never again compete for the same invocation. The retirement note records what happened rather than tidying it away, which is why it can be quoted here. The health check now fails outright if any skill instructs a run of a skill that no longer exists, which is how that class of dangling reference gets caught mechanically instead of by a human noticing unfamiliar names.

What it cost. One full review cycle discarded and re-run, and the owner's stated response was that he had believed this component was already built and ready to use. That is the expensive part. A wrong number can be corrected. A review that confidently reports the wrong reviewers teaches its reader to check everything by hand, which removes most of the value of having the system at all.

What is still wrong today, and how it is guarded

The tier 2 seam, confirmed on a second task

Writing this teardown found the same failure again. The daily content research task last wrote to its output file on August 24, 2026. It has fired roughly thirty-six times since, reported SUCCEEDED every time, and produced nothing. Its owner believed as of the day before writing that it was still working, and made a decision about which task to pause based on that belief.

Guarded by: a staleness check that already caught it. The health check flags the file at 12 days against a 7-day bar, and the daily brief carries the same signal. The detector worked. The gap was that a WARN does not reach the phone, so the signal sat in a report nobody had opened. Not yet fixed.

Context caps are documented but not enforced

A written policy with no mechanism, in a system whose entire thesis is that mechanisms beat intentions.

Guarded by: nothing. This is an unguarded gap and naming it is the current state of the mitigation.

The system has no gate

A kill criterion was set in July requiring a demonstrated saving of at least two focused hours per week. It was closed this month as UNMEASURED rather than passed or failed. The reason is instructive: the metric asked how many hours the system saved during weeks largely spent building the system, which is not answerable, and a bar that cannot be read is not a bar. It went unlogged for forty-five days because it was unanswerable, not because it was ignored.

Guarded by: the replacement requirement, which is that any successor criterion must be readable from files by a script rather than by introspection. Until one is chosen there is no gate, and that absence is recorded in the decision log as a thing to watch.

A dated bug nobody has hit yet

Around week six of the current season, a year field used for statistics will roll forward while a related field used for usage data stays pinned to the prior year, and the data layer carries no marker distinguishing them. It is documented, dated, and unfixed.

Guarded by: being written down with its trigger condition, and scheduled for repair before the season deadline. That is weaker than a check and is acknowledged as such.

Sixty-four predictions recorded, zero settled

The system records dated predictions and has never scored one. A ledger that only ever accumulates is not calibration, it is bookkeeping.

Guarded by: a health-check line that reports the ratio on every run, so the gap is visible daily rather than discovered later.

What is deliberately not here

Two active workstreams are excluded because neither can be described without naming a family member's professional work or household financial matters. One is a document-drafting tool, the other a budgeting application. Both are real and both are outside the scope of what is described above. No client is named anywhere in this document, and the inbox triage task described above filtered on specific named individuals whose names are withheld.

One operational metric

24 hours. The maximum time between a covered failure and its owner being told.

The health check runs once daily at 08:15 under launchd. Only FAIL lines are forwarded to the alert channel. So for any failure mode that has a check with a threshold, the worst case between breakage and notification is one scheduling interval.

This holds for a year because it is a property of the schedule and the exit-class rule, not of anything that grows. It does not drift with the number of skills, the number of scheduled tasks, the size of the codebase, or how much the system is used. Adding checks does not change it. It is falsifiable by reading the job's log against the alert history, and it fails honestly the moment the job stops running, because a missing log is itself detectable.

The bound that makes it meaningful

The metric is worth stating precisely because of the word covered. Failures without a check ran for fifty days and twelve days respectively, and both were found by a human noticing something, not by the system reporting it. The distance between 24 hours and 50 days is the entire value of writing a check, and it is why the honest version of this number carries its qualifier rather than being rounded up into a claim about the system as a whole.

Which suggests the single sentence this teardown reduces to. A status field reports that a process finished. It does not report that the work landed. Those are different questions, and every silent failure documented here came from a system that had quietly stopped telling them apart.