A model knows things. A coworker knows what is going on.
The Aug 14 review said beckett loses work at the door; v7.1.0 fixed the door. This report is about the next thing, and it starts from a repositioning: Kowo is the work data plane for AI coworkers; Beckett is the first coworker built on it. Not two businesses — one stack. The question this page tries to answer with receipts is whether that is a story retrofitted around beckett or a thing beckett's own disk already contains. Short version: the raw exhaust is already on disk in eleven stores, one real episode already runs the full Kowo loop end to end, and the destination tables already exist in beckett.db with zero rows. What is missing is the join, the objects, and a buyer.
events/dispatch.jsonl — the closest thing to a work ledger today (1,144 keyed by run id, 585 by legacy ticket id)beckett.db's event and job tables — the schema is there, the ledger never moved inOne stack, not two companies
"A data pipeline company plus a coworker company" sounds like two businesses. Make it one: Kowo = the work data plane. Beckett = the software-engineering coworker built on it.
An AI coworker needs more than access to Slack, GitHub and Linear. It needs a continuously updated understanding of what the company is doing, what changed, what belongs together, what needs action, what it is allowed to do, and what happened the last time it acted. Today every agent — beckett included — reconstructs that state on wake-up, from scratch, with tokens. That reconstruction is the product Kowo should own.
Airbyte already calls itself "a data and context layer for AI agents"; Glean is building context graphs from enterprise activity; Dust wires company knowledge to agents. Competing horizontally on "we connect your data to agents" is a fight against connector catalogs and enterprise distribution. Kowo can own something narrower and more actionable: turning the exhaust of engineering work into executable work state — events → objects → episodes → state — for a small set of sources (GitHub, Discord/Slack, Linear, CI, Sentry) and make the cross-source understanding unusually good before adding a sixth.
The rest of this page tests that claim against the disk: is there a real episode that already runs the loop? What raw material exists and in what shape? What would it take to produce the "second object" — the curated WorkItem — from the five raw events? And, because the CTO-seat memory records two vetoes in one conversation on 16 Aug (supply is not a moat; name who pays), what does Kowo sell?
One real episode, rendered as Kowo would hold it
Picked because it is the most recent done run in runs.json whose prompt cites other work — and it turned out to be the whole loop: a routine drives a browser run, the run hits a bug, a second run hits it again, beckett notices "second run in a row", files its own fix run, sonnet implements and reviews it, it lands, deploys, and beckett reports it. No human message between the second failure and the deploy. Every line below is from disk.
The exhaust, as it sits in six stores
Sources: routines.json · browser-agent/<id>/journal.jsonl ×2 · runs.json · events/dispatch.jsonl · spend.jsonl · journal/run-….log · channels/1520986792373911622.jsonl · uptime.jsonl · GitHub. Handle redacted where the source redacted it. Times UTC.
- 2026-08-15 16:08:10routines.json → browser-agent
x-social-morning(daily, 09:00–10:00 PT) dispatches browser runfd4140ea: "Log into X … compose one fresh post". 45 journal events, 42 evals. Twostrict mode violation: locator('div[role="dialog"]') resolved to 2 elementserrors — the compose dialog reopened after posting. Result: done, tweet live at 16:12:41. - 2026-08-16 01:16:09routines.json → browser-agent
x-social-eveningdispatches browser run59983c26. First submit silently fails (text set with.fill(), Post button never enables); the agent retypes with keystrokes, X flags "Whoops! You already said that". Result: done at 01:18:48 — but only because the agent improvised. - 01:19:18.419runs.json Run
run-20260816-fix-flaky-x-compose-keystrokes-not-fillcreated.requesterId: null. Prompt: "Two background browser runs in a row hit the same failure mode … this is a class bug … Run 1 (fd4140ea…) … Run 2 (59983c26…)". Four "done means" criteria, a ceiling ("do not refactor the browser agent"). Filed 30 s after the second browser run finished. - 01:19:18–01:19:24dispatch.jsonl 20 rows for this run:
run:deploy queued → implement:staff admitted → repo passed → worktree passed → implement started worker wk_0f93bebe on claude. - 01:19:25 · 01:19:28channels/#general beckett: "posted the one about a maintainer's rce disclosure … https://x.com/i/status/2088797…" · "the compose box fought me again though, second run in a row. setting the text programmatically leaves x's post button disabled, so the first submit silently …"
- 01:24:22spend.jsonl
{stage:"implement", model:"claude-sonnet-5", effort:"high", turns:41, toolCalls:79, tokensIn:3,864,056, costUsd:2.376, outcome:"done", sessionId:"df430f8e…"}. dispatch:implement:verdict passed → review:staff admitted → worktree reused → review started worker wk_5502559c. - 01:28:02journal/ + spend.jsonl Review PASS: "the diff meets all four 'done means' criteria via prompt/instruction changes in both live paths (src/agent/invoke.ts and docs/social.md), with tests added for the testable code path".
{stage:"review", model:"claude-sonnet-5", turns:23, costUsd:0.657}. - 01:28:06dispatch.jsonl + GitHub
git-push passed → done passed→ kowo-co/beckett@0e196c3. (Direct push via outbox — this was v7.0.11, one release before publish-opens-a-PR shipped.) - 01:29:43 → 01:29:44uptime.jsonl
clean_shutdown→boot: v7.0.12 deployed. - 01:30:04channels/#general beckett: "x compose fix landed and it's live now, v7.0.12. it types the post like a human instead of setting the value, and verifies from a fresh page instead of trusting that dialog: https://github.com/kowo-co/beckett/commit/0e19…"
The same episode as a work ledger
This is what the exhaust looks like once every store emits into one append-only table with one shape. Nothing here is invented — every row maps to a line above; the only additions are the verb.object vocabulary and the correlation column, which is exactly the part that does not exist today.
| ts (UTC) | source | actor | verb.object | object id | correlation | today lives in |
|---|---|---|---|---|---|---|
| 08-15 16:08:10 | routine | beckett/routines | routine.fired | x-social-morning | → browser:fd4140ea | routines.json state (last fire only, overwritten) |
| 08-15 16:08:10 | browser | beckett/browser-agent | browser_run.started | fd4140ea | routine:x-social-morning | browser-agent/…/journal.jsonl dispatched (routine id not recorded; context:false) |
| 08-15 16:09:11 · 16:11:05 | browser | beckett/browser-agent | browser_run.error | fd4140ea | — | journal eval ok:false "strict mode violation … role=dialog resolved to 2" |
| 08-15 16:12:41 | browser | beckett/browser-agent | browser_run.finished | fd4140ea | artifact: x.com/…/status/2088659… | journal finished state:done result:"…" |
| 08-16 01:16:09 | routine | beckett/routines | routine.fired | x-social-evening | → browser:59983c26 | routines.json |
| 08-16 01:18:48 | browser | beckett/browser-agent | browser_run.finished | 59983c26 | artifact: x.com/i/status/2088797… | journal finished — the .fill()/retry story is prose in result |
| 08-16 01:19:18 | run | beckett/concierge | run.created | run-20260816-fix-flaky-x-compose… | caused-by browser:fd4140ea, browser:59983c26 | runs.json — the two ids appear only inside the prompt text |
| 08-16 01:19:24 | run | wk_0f93bebe (sonnet-5) | run.stage.started implement | run-2026… | session:df430f8e | dispatch.jsonl |
| 08-16 01:19:28 | discord | beckett | message.posted | msg 1537722… #general | about: browser:59983c26 (inferred) | channels/<id>.jsonl — no run/browser id on the row |
| 08-16 01:24:22 | run | wk_0f93bebe | run.stage.finished implement · $2.376 | run-2026… | session:df430f8e | dispatch.jsonl + spend.jsonl (joined on ticketId=run id and sessionId) |
| 08-16 01:28:02 | run | wk_5502559c | run.review.passed · $0.657 | run-2026… | session:36b263f8 | dispatch.jsonl + spend.jsonl + journal/*.log |
| 08-16 01:28:06 | github | 0x-beck[bot] | change.landed | kowo-co/beckett@0e196c3 | run-2026… | runs.json prUrl/published; GitHub itself (not polled into anything) |
| 08-16 01:29:44 | daemon | beckett/deploy | deploy.completed v7.0.12 | beckett | change:0e196c3 (inferred) | uptime.jsonl boot — version not on the row |
| 08-16 01:30:04 | discord | beckett | message.posted | msg … #general | about: run-2026… (URL in text) | channels/<id>.jsonl |
Note what "correlation" costs today: five of the fourteen rows have a link that exists only as free text (a run id typed into a prompt, a commit URL inside a chat message, "second run in a row" as English). Kowo's job in one sentence is to make that column a column.
The same episode as work objects — the "second object"
The important product isn't storing the fourteen events. It is producing these. Seven canonical objects for the SWE wedge (Actor · Project · WorkItem · Change · Conversation · Incident · Run); this episode instantiates five of them, plus the trigger that should have fired instead of a model noticing.
- Project
- beckett · social lane (routines x-social-morning / x-social-evening)
- Signal
- 2 browser runs in 9 h with the same failure class (compose dialog stuck;
.fill()leaves Post disabled) - Occurrences
- browser:fd4140ea (08-15 16:09Z, 16:11Z) · browser:59983c26 (08-16 01:17Z)
- Impact
- both posts still published; second attempt tripped X's duplicate guard — near double-post
- State
- resolved by run:fix-flaky-x-compose → change:0e196c3 → deploy v7.0.12
- Detected by
- beckett (concierge), reading two
resultstrings — not by a query
- Goal
- X compose types via keystrokes, never
.fill(); verify publication from a fresh page; stuck-dialog + "Whoops" handled explicitly; test if testable - Origin
- incident:x-compose-fill · filed by beckett, requesterId null
- Owner
- beckett
- Ceiling
- targeted fix; no browser-agent / session-manager / scheduler refactor; no new deps
- State
- done · proven by review PASS on 4/4 criteria
- Cast
- implement sonnet-5/high (wk_0f93bebe, session df430f8e) · review sonnet-5/high (wk_5502559c, session 36b263f8)
- Cost
- $3.03 · 64 turns · 101 tool calls · 4.93 M tokens in
- Duration
- 8m 48s (01:19:18 → 01:28:06) · 0 review cycles · 0 continuations · 0 parks
- Produced
- change:0e196c3
- Branch
- beckett/run-fix-flaky-x-compose-keystrokes-not-fill · base 6a13001
- Touched
- src/agent/invoke.ts (X posting instructions + tests) · docs/social.md (the two static-task routines)
- Landed
- direct push to main via outbox (pre-B3) · deployed as v7.0.12 at 01:29:44Z
- Resolves
- wi:fix-flaky-x-compose · incident:x-compose-fill
- Messages
- 3, all beckett: tweet posted · "compose box fought me again, second run in a row" · "x compose fix landed and it's live now, v7.0.12"
- Decisions
- none by humans; the fix was beckett's call inside doctrine (reversible, own repo)
- About
- browser:59983c26 · run:fix-flaky-x-compose · change:0e196c3
- WHEN
count(browser_run.error where class = X, project = P, window = 24h) ≥ 2- THEN
- file WorkItem "class bug in <flow>" with both occurrences attached, cast sonnet, ceiling = the flow
- Today
- this exists as beckett's judgment reading English. It worked. It is not repeatable, queryable, or something a customer can write.
The context pack — the briefing a coworker would wake up to
The killer API is kowo.context.for(…). For this episode, the second browser run's failure should have produced this packet for whoever picks it up — beckett, or a human, or the next routine — instead of each of them re-reading six stores.
{
goal: "X compose posts reliably: keystrokes, verify from fresh page, never double-post",
current_state: "2 occurrences in 9h, both runs completed by improvising; no fix in flight",
project: "beckett / social lane", owner: "beckett",
occurrences: [ browser:fd4140ea @08-15T16:10Z (dialog stuck ×2),
browser:59983c26 @08-16T01:17Z (.fill() → Post disabled → retype → 'Whoops') ],
relevant_code: [ "src/agent/invoke.ts (X posting task text)", "docs/social.md (static routine tasks)",
"src/browser/betterwright.ts (pressSequentially available)" ],
related_changes: [], ← nothing has touched this path since 6a13001
decisions: [ "reversible + own repo → no approval needed (doctrine)" ],
people: [ ro: owner of the X account and the social lane ],
recent_conversations: [ #general 08-15 19:37–23:41 (babble PR review, TPS graph — unrelated) ],
previous_agent_runs: [ 0 runs matching 'compose' or 'x.com' in runs.json ],
triggers: [ x-social-morning 09:00 PT, x-social-evening 18:00 PT — next fire in ~15h ],
permissions: [ credsEntry x-account (jingle) · repo kowo-co/beckett write via 0x-beck ],
budget_hint: "median run on this box: sonnet-5, ~$3, ~9 min"
}Read the empty fields. related_changes: [] and previous_agent_runs: 0 are the honest answer today, and they are already useful: they tell the coworker this is a fresh class of bug, not a regression, which changes the cast and the ceiling. Three weeks from now, the same call should return "you fixed this in 0e196c3; X changed the compose DOM again" — and that is organizational memory no model has.
What Kowo would have added, concretely
| Gap in the episode | Evidence | The Kowo primitive that closes it |
|---|---|---|
| The two browser runs and the fix run are linked only by ids typed into a prompt | runs.json prompt text; no deps/caused-by edge; browser journal has no run/routine id (context:false) | correlation_ids on every event; Incident.occurrences; Run.caused_by |
| "Second run in a row" was noticed by a model reading two result strings | the observation is beckett's #general message, 10 s after the run was filed | a Trigger that is a query over browser_run.error, not a judgment call; the model then decides, it doesn't detect |
| Nothing remembers the fix | grep -ri "compose\|x.com" memory/ → 0 nodes; the only trace is a chat message and a commit | Run.outcome written back into work state; a Change that resolves an Incident becomes retrievable by class |
| The routine that caused the run is not on the run | routines.json keeps only lastFiredAt; browser run's dispatched event carries the task text, not the routine id | routine.fired event with the child id; Trigger→Run edge |
| The deploy is a boot line with no version | uptime.jsonl {kind:"boot"}; v7.0.12 known only from the chat message | deploy.completed {version, change} — the event Sentry-style incident matching needs |
| Cost, stages and transcript are three files with two different keys | spend.ticketId holds a run id; dispatch has runId; journal is a filename; sessionId is the only shared key across all three | one event table, job_id foreign key — the schema that already exists in beckett.db |
What we actually have — the translation table
Every persistent surface the daemon writes today, mapped onto the Kowo primitive it would feed. Graded honestly: present the data is there in usable shape · partial there but lossy, cursor-only or wrongly keyed · missing not collected. Full inventory with file:line refs in the appendix.
Surfaces → primitives
| Store (today) | Rows / size | Kowo primitive | What it yields | Grade | What it takes |
|---|---|---|---|---|---|
events/dispatch.jsonlsrc/dispatch/events.ts | 1,729 rows | Event (run.stage.*) | every stage transition of every run and deploy, outcome-tagged, timed | present | rename ticketId→job_id (585 legacy rows), add verb.object, insert into event |
runs.jsonsrc/run/store.ts | 51 runs | Run + WorkItem + Change | intent, cast, sessions, blocker/question/proof, PR/commit URL, deps, files | present | split: WorkItem = title+prompt+criteria; Run = cast+sessions+cost; Change = branch+prUrl. Mutable → derive from events going forward |
spend.jsonlsrc/spend.ts | 167 rows · $813 | Event (run.stage.finished cost) | per-stage cost, tokens, turns, model, outcome, sessionId | present | join on run id + sessionId; becomes event.cost_usd/tokens. Concierge and browser rows still absent |
journal/*.logsrc/progress/journal.ts | 69 files · 1.6 MB | Run.transcript (artifact) | tool-by-tool play-by-play per stage; the review verdict text | partial | keyed by filename only; store as artifact ref on the Run, extract the verdict as an event |
channels/<id>.jsonlsrc/concierge/channel-context.ts | 14 ch · 294 msgs | Conversation + Event (message.posted) | author, content, reply-to, mentions, kind user/beckett | partial | bounded + pruned — history is lost; no run/PR/incident link on rows. Ledger must outlive the context window; resolver extracts URLs/ids into about edges |
browser-agent/<id>/journal.jsonlsrc/browser/agent.ts | 49 runs · 174 MB | Run (browser) + Event (browser_run.*) | dispatched task, every eval (ok/error/url/ms), finished result, artifacts | partial | no parent (routine/run/message) id; errors are strings — needs a class field for triggers to count |
routines.jsonsrc/routine/store.ts | 8 routines | Trigger | schedule/watch definitions + lastFiredAt | partial | definitions are triggers already; fires are overwritten, not logged → emit routine.fired {child} |
memory/*.md (+ Moss index)src/memory/index.ts | 14 nodes · 5 types | Actor · Project · decisions · calibration | people, projects, preferences, typed edges (supersedes/caused-by/about/contradicts/part-of) | present | already the right shape for the curated layer; needs a Run.outcome → memory writeback and to be queryable by object id, not just prose |
uptime.jsonl | boots/shutdowns | Event (deploy.completed) | boot / clean_shutdown timestamps | partial | add version + change sha to the boot row |
github-prs.json · github-activity.jsonsrc/github/poll.ts · activity.ts | cursors ({} today) | Event (pull_request.*, review.*, push) | watermarks: seen review/comment ids, last commit sha, last merged PR | missing | the poller sees the events and keeps only the cursor. Persist each observed event; later, a webhook receiver (0 today) |
tasks.json | legacy | WorkItem (v6) | ticket tree with branch/PR/publication links | partial | backfill once as historical WorkItems, then freeze |
proposals/ | 0 | WorkItem (kind product-idea / doctrine) | claim, rationale, provenance, decided/became | present | already append-shaped; add proposal.decided events |
secret-redemptions.jsonl · jingle | audit rows | permissions on Event/Actor | which entry, which fields (names only), destination kind | partial | becomes the permissions column: what this actor may touch, evidenced by what it did touch |
quick/ · free-time/ · concierge-sessions/ | 5 · 1 · 9 | Run (errand / free-time / conversation session) | reports, writebacks, session ids | partial | same treatment as runs: an event on start/finish, artifact refs |
beckett.db — job, event, node, edge, health, kv | 0 rows (kv: 2) | the destination | event(job_id, kind, payload, cost_usd, tokens, ts) already indexed by job and kind | partial | exists, unused. Widen job_id to object_id (runs are one object kind among seven) and start writing |
| Linear · CI (GitHub Actions) · Sentry · deployments outside beckett | — | WorkItem · Change.ci · Incident · deploy | — | missing | the v0 source list from the thesis. Not needed to prove the model on kowo-co's own work; needed for a customer |
Join keys, honestly
The whole "curation" story lives or dies on whether rows from different stores can find each other. Today:
| Key | Appears in | Joins | Verdict |
|---|---|---|---|
run id (run-YYYYMMDD-slug) | runs.json id · dispatch runId · spend ticketId (yes, that field) · journal filename · worktree path | run ↔ stages ↔ cost ↔ transcript | present — the spine. Field names differ per store |
sessionId | runs.json sessionIds.{implement,review} · spend sessionId · concierge-sessions | run stage ↔ spend row ↔ Claude session | present |
channelId | runs.json · channels/<id>.jsonl · progress-cards · concierge-sessions | run ↔ the channel it was born in | partial — channel, not message; you know where, not which message asked |
messageId | channels rows · task cards | — | missing as a link from a run/incident back to the message that caused it |
browser run id | browser-agent/<id> · nowhere else | — | missing — no parent routine, no parent run, no channel; the episode's link is prose |
routine id | routines.json only | — | missing — fires overwrite; children don't know their trigger |
prUrl / commit sha | runs.json prUrl, published.prUrl · chat text | run → change | partial — the URL is a string; there is no Change record and GitHub state (checks, reviews, merge) is not pulled back |
[[wikilink]] names | memory/*.md | node ↔ node (typed) | present inside memory; missing from memory to any run/change/incident id |
The collection plan — four extractions from beckett into a small kowo substrate
Make beckett consume these APIs instead of its private stores. Each extraction has a backfill from what is already on disk (so it shows real state on day one, not an empty dashboard), a write path going forward, and a gate phrased as a query that must return the right answer.
E1 · Events: one append-only ledger, backfilled from eleven stores
- Shape:
event(id, ts, source, actor, verb, object_type, object_id, project, payload, permissions, correlation[])— the existingbeckett.db eventtable withjob_idwidened to(object_type, object_id)and acorrelationJSON column. - Backfill (a one-off
kowo backfill, read-only against the sidecars): dispatch.jsonl →run.stage.*· spend.jsonl → cost onto the matching stage-finished event · runs.json →run.created/done/parked+change.landedfrom prUrl · channels/*.jsonl →message.posted· browser-agent journals →browser_run.started/error/finished· routines.json → the definitions (fires can't be recovered) · uptime.jsonl →daemon.boot· memory nodes →memory.written· proposals →proposal.*. - Write path: the
DispatchEventBus.emitinsrc/dispatch/events.ts,appendSpend, the channel-context writer, the browser agent's journal writer, and the routine runner each also insert one row. No store is deleted in E1; the ledger is a second writer until it is trusted. - Extractors for the "correlation" column: run ids, browser run ids, commit shas, PR URLs, tweet URLs and
[[wikilinks]]pulled out of prompt/result/message text with a regex — this alone links every row in the Part 2 episode.
beckett events --object run-20260816-fix-flaky-x-compose --with-correlated prints all fourteen rows of the Part 2 ledger from one query, including the two browser runs and the two chat messages, with no store opened by hand. Measured: backfill row count ≥ 1,729 + 167 + 294 + (49 × ~30) ≈ 3,700 events; zero rows with an empty object_id.E2 · Work objects: seven kinds and the resolvers that produce them
- Kinds:
Actor · Project · WorkItem · Change · Conversation · Incident · Run, stored in the existingnode/edgetables (typed edges already inRelationType: supersedes · caused-by · about · contradicts · part-of; addresolvesandproduced). - Resolvers are code, not prompts:
run.created→ WorkItem + Run ·change.landed→ Change, edge Run→produced→Change · a message whose text contains an object id → edge Conversation→about→object · Nbrowser_run.errorwith the sameclassin one project within 24 h → Incident with N occurrences · a Run whose WorkItem's origin is an Incident → edge Change→resolves→Incident. - The one model call allowed: classifying free-text errors and results into a
class(haiku, cached, idempotent) — because "strict mode violation: role=dialog ×2" and ".fill() left Post disabled" are the same incident and only a model knows that. - Actor / Project: seed from memory nodes (people, projects),
identities.json,workspaces.json, and the repo list — these already exist, they just aren't addressable by id.
beckett work show incident:x-compose-fill-2026-08 renders the five objects from Part 2 with their edges, generated by resolvers over the E1 ledger, not by hand. Measured: every done run in runs.json has a WorkItem, a Run, and (if prUrl) a Change; every run whose prompt cites another id has a caused-by edge.E3 · Context packs: kowo.context.for(object), and the concierge uses it
- Assembler: given any object id (or a message, or a PR URL), walk the graph one or two hops and fill the fixed packet: goal · current_state · relevant_code · related_changes · decisions · people · recent_conversations · incidents · previous_agent_runs · triggers · permissions · budget_hint. Empty fields stay in the packet — "no prior runs" is information.
- First consumer is beckett itself: the concierge memory primer (v7.2.0,
[concierge.memory_primer]) is already "helpful memories ride the turn". Swap its input from prose recall alone tocontext.for(the object this turn is about). Runs get the packet in the worker brief. That is the moment beckett depends on Kowo. - Surface:
beckett context <id|url>, an MCP tool of the same name, and a JSON endpoint on the daemon's existing control socket. No cloud, no auth story yet.
git log / grep / ls to rediscover context that the packet already carried. Measured: median implement tokensIn for runs with a packet vs the 30-day baseline (this box: ~3.9 M for the Part 2 run); the target is a measurable drop, not a feeling.E4 · Triggers as queries over work state
- Rule shape:
WHEN <predicate over objects/events> [FOR <duration>] THEN <file WorkItem with template + cast + ceiling>, idempotent by(trigger_id, fire_key)asdocs/initiative.mdalready specifies. - First four rules, all from real pain on this box: two browser errors of one class in 24 h → Incident + repair WorkItem (Part 2) · a run
parkedon a typed blocker for > 4 h with no human reply → owed-notification WorkItem · a landed Change with nodeploy.completedafter 30 min → "merged-but-not-live" WorkItem (the Aug 12 wrong-claim class) · a routine that failed twice in a row → disable + WorkItem. - Routines become triggers: the 8 rows in routines.json are already
schedule/watchtriggers; they move onto the same table and start emittingroutine.fired {child}.
origin is a trigger ≥ 1/day, and zero duplicate fires.What Kowo sells
"I'm not sure what Kowo does outside of providing beckett, which is open source and therefore generates no revenue." Correct as stated — so answer it as stated. Beckett being open source is the distribution, not the leak. What is sold is what open source cannot give a team: their own work state, running, watched, and improving.
The bar you set on 16 Aug (from the memory graph)
Two vetoes in one channel conversation, both saved as calibration nodes: "our data pipeline is not quality at all, so we don't have anything to offer that Reddit or Twitter can't" (supply is not a moat) and "we already can't make money off of you… maybe something that's marketable as a service we provide. Like idk consulting" (name who pays, how much, how soon — before the moat argument). Every lane below is written to that bar. Where a number is a hypothesis it says so; nothing on this page has a customer yet.
Three lanes, ordered by time-to-cash
A · Coworker install — a service, billed now
"We put an engineer in your Discord/Slack that notices and fixes things" — done as an engagement: install beckett on the customer's box or a Kowo-managed one, wire GitHub + chat + CI, write their first six triggers and their company.md, and stay on retainer while the state accrues.
How much (hypothesis): setup $3–8k, then $1–2k/month retainer + pass-through model spend. Comparable: Devin $20 + $2/ACU, "AI employee" seats $250–2,500/mo — but those are seats, not an installed teammate with memory.
How soon: the first pitch can happen the day Part 2 is a page on the site. Nothing in Part 4 blocks it; E1–E3 make each engagement compound.
B · Kowo Node — open-core hosting and management
Beckett stays MIT-open. The paid thing is the node: hosted or managed on the customer's infra, with the ledger, work graph, secrets, permissions, triggers and agent runtime local — and an optional Kowo Cloud for management, observability, updates, billing, fleet admin.
How much (hypothesis): $200–500/month per node + model spend; enterprise pricing for the "your work never leaves your environment" story.
How soon: after E1–E3 and a one-command installer; realistically 60–90 days behind A.
C · Kowo as substrate for other coworkers
events.subscribe · work.get · context.for · timeline · trigger.create · run.record plus an MCP surface. Beckett is coworker #1; a support coworker, an ops coworker, or a customer's own agent are the next consumers. "Build your own coworker on Kowo."
How much: metered on context packs and trigger evaluations, or a platform fee per coworker.
How soon: not before there is a second coworker that isn't ours, or a customer asking. This is the platform expansion, not the wedge.
The moat that isn't supply
The commodity-data veto was right: Kowo will never out-collect anyone. The asset that accrues in a Kowo node is not volume, it is a dataset nobody else can have for that customer:
human activity (messages, reviews, decisions) + company state (work items, changes, incidents, deploys) + agent activity (runs, casts, tool calls, cost) + agent outcomes (proof, review verdicts, what shipped, what broke after) ──────────────────────────────────────────────────────────── = "when this service fails after this kind of migration, these three checks find it" "PRs touching auth need Samantha" "this workflow succeeded 18/21 times; the 3 failures were all X" "you fixed this class of bug in 0e196c3 three weeks ago"
That last line is already true on this box for exactly one incident, and it took eleven minutes and three dollars to create. It is organizational memory the customer owns, in their environment, that gets more valuable every week they keep the node — and it walks out the door if they churn. That is a retention moat, not a supply moat, and it passes both vetoes: it is not "we collect data", it is "your coworker remembers what happened when it acted last time, and nobody else's does". Your own market-research doc already names memory/company knowledge as the line between a tool and a coworker; Kowo is the place that memory lives.
company.md, which is currently a template with every section empty.The next 30 days
Two tracks that don't wait on each other: make the data plane real on kowo-co's own work (E1–E3), and put a buyer in front of the episode.
1 · The ledger, backfilled, and one query that reproduces Part 2
- E1:
eventtable widened;kowo backfillover the eleven stores; second-writer hooks in dispatch/spend/channel/browser/routine paths - Correlation extractors for ids/URLs in free text
beckett eventsCLI
2 · Objects + the first context pack, consumed by the concierge
- E2 resolvers for Run/WorkItem/Change/Conversation/Incident; haiku error-class classifier
- E3
context.forassembler; the memory primer takes it as input; workers get it in the brief - Measure implement tokensIn before/after
beckett context incident:x-compose-fill-2026-08 returns the Part 2 packet from resolvers; a new run's worker starts with it. Week 2–4.3 · One trigger, and one paid conversation
- E4: the "two browser errors of one class" rule, replayed against the ledger, then live
- Site: Kowo positioning page (the internal definition, the pipeline diagram, the Part 2 episode as the demo), Beckett as "the first coworker"
- Lane A pitch to a named team; fill
company.md; write the falsification number
Not in the 30 days: Linear/Sentry/CI connectors, Kowo Cloud, webhooks, multi-tenant anything. All of it is downstream of proving the model on our own exhaust and finding out whether lane A sells.
Questions only you can answer
Beckett's own CTO seat is briefed to say "I don't know X and this decision turns on it" rather than invent a number. Same rule here.
- Is "ro" in the channel logs you? The two vetoes are attributed to ro; this page treats them as your bar. If that's someone else, the revenue section needs a second reading.
- Which team is the first lane-A pitch? A name, or "nobody yet" — both are answers; the second one changes the 30-day plan to "find one" before "build E3".
- Is Kowo willing to be a services company for two quarters? Lane A bills first and productizes later. If the answer is no, lane B moves up and needs the installer before anything else.
- Which of the portfolio (beckett, babble, chilltext-arena, beckett-metrics, fal-pipelines, freebuff, pixe-eval, betterwright-bench, 0xbeckett.me) is on the Kowo story and which is not? The Aug 14 review said the CTO seat has no company object;
company.mdis still a template. Answering "what Kowo is" in one paragraph there unblocks beckett's own free-time thinking about it. - Do we keep the events under
~/.beckettor is Kowo a separate process/package from day one? This page assumes "same daemon, new module, second writer" for E1–E3 and a real boundary only when a second consumer shows up. If you want the boundary now, say so and E1 becomes a package first. - Which of the six missing surfaces matters for the first customer — Linear, CI, Sentry? As far as this box shows, kowo-co doesn't use Linear or Sentry, so the model can't be proven on them here. The first customer decides connector #2.
Closing
The Aug 14 review found five roots and shipped the fixes for four of them in a weekend; the fifth — no company object, a CTO seat that proposes its own parts — is what this page is actually about, seen from the other side. Beckett doesn't have a company to think about because Kowo hasn't said what it is. The cleanest thing to say is: Kowo is the data plane for digital coworkers, and Beckett is the first one.
The evidence that this is not a retrofit is on the disk. Eleven stores already emit the exhaust. The event and job tables already exist, empty. One routine that runs twice a day already produced a complete incident → work item → run → change → deploy → report loop with no human, in eleven minutes, for three dollars — and the only reason it isn't a Kowo record is that the links between the pieces are English. Making those links a column, those pieces objects, and that noticing a query is a backfill script, a resolver module, and one better input to a primer that already exists.
The revenue answer is not "Kowo Data as a horizontal platform". It is: lead with Beckett as the reason to install Kowo, sell the install as a service because services bill on day one and you said so, let every engagement leave the same node behind, and let the node be the thing that gets more valuable per customer per week — because it holds what happened the last time their coworker acted, and nothing else does. A model knows things. A coworker knows what is going on. Kowo is the second part.
"i want to begin thinking of kowo as a company i can posit as valuable and then make and show real value."jason, 16 Aug 2026 — Part 2 is the first piece of real value; Part 4 is how to make it repeatable; Part 5 is who might pay for it. The next thing to make is a buyer.
Inventory + method
Every persistent surface, with where it is written. Numbers are from ~beckett/.beckett on 16 Aug 2026 (daemon v7.2.0). Prepared read-only; nothing was modified on the box.
| Path | Writer | Shape | Append-only? |
|---|---|---|---|
runs.json | src/run/store.ts | {version:1, runs:Run[]} — id/slug/title/prompt/channelId/state (10-value enum)/cast/branch/sessionIds/reviewCycles/blocker/question/proof/published/deps/files | mutable, whole-row patch |
events/dispatch.jsonl | src/dispatch/events.ts (DispatchEventBus.emit) | {ts, runId|ticketId, runRef, branchRef, channel?, stage, outcome, elapsedMs, message?, error?} | yes |
spend.jsonl | src/spend.ts | {ticketId, project, stage, harness, model, effort, turns, toolCalls, tokensIn, tokensOut, costUsd, durationMs, outcome, reviewTier, ts, sessionId?} | yes (O_APPEND + fsync) |
journal/<run>.log | src/progress/journal.ts | timestamped lines per stage/worker | yes |
channels/<channelId>.jsonl | src/concierge/channel-context.ts | {messageId, ts, authorId, authorName, content, repliedToId?, mentions?, kind:user|beckett, forwardedSnapshots?} | append with cap + prune |
browser-agent/<id>/journal.jsonl + artifacts/ | src/browser/agent.ts | {ts, kind:dispatched|leg|eval|finished, task|ok|url|error|state|result} | yes |
routines.json | src/routine/store.ts | {routines:[{id, name, action:{kind:schedule|watch,…}, state:{lastFiredAt,…}}]} | mutable |
memory/**/*.md (+ .moss cache) | src/memory/index.ts | frontmatter node (name/type/description/metadata) + body + typed wikilinks; archive/ never delete | archived, never deleted |
uptime.jsonl | src/uptime.ts | {kind:boot|clean_shutdown, at} | yes |
github-prs.json · github-activity.json | src/github/poll.ts · activity.ts | cursors: seenReviewIds, seenCommentIds, lastCommitSha, lastMergedPrNumber | mutable snapshot; no webhooks anywhere in src/github |
tasks.json | src/task/store.ts | legacy v6 ticket tree with branch/PR/publication links | mutable |
proposals/<id>.json | src/proposal/store.ts | kind, claim, rationale, provenance[], origin, status, decided, became | append-shaped |
secret-redemptions.jsonl · secret-requests.json | src/secret/intake.ts | {ts, event, destination.kind, entry?, fields:[names]} | yes (names only) |
quick/ · free-time/ · concierge-sessions/ | src/quick · src/freetime/run.ts · src/concierge | reports, writeback.json, session json | mixed |
identities.json · workspaces.json · access.txt · progress-cards.json · owed-* json | src/discord/* · src/progress/cards.ts · src/concierge/owed-* | actor map, channel↔project bindings, access grants, card anchors, owed replies | mutable |
beckett.db | schema only — no code opens it (grep new Database( → 0) | job(id, parent, title, intent, criteria, runner, cast, deps, state, …) · event(id, job_id, kind, payload, cost_usd, tokens, delivered_at, ts) · node · edge · health · kv | 0 rows (kv: 2) |
ops-log/ | src/ops-log/* | Discord log-line mirror only — writes nothing to disk | — |
Method and caveats
The episode in Part 2 was found by taking the most recent done run in runs.json with a PR/commit URL and following every id in its prompt; all timestamps, costs and quotes are copied from the files named. The context pack and the objects are assembled by hand to show what the state supports — no resolver exists yet; that is the point of Part 4. Store counts: spend.jsonl has blank separator lines (334 lines, 167 records); memory has 14 markdown nodes (11 public), the earlier "128 files" figure in the session counted the Moss cache. Competitor positioning (Airbyte, Glean, Dust) is from public docs as of the Aug 14 review. Prices in Part 5 are hypotheses to test, marked as such. One sourced design reference: docs/orchestration.md and docs/initiative.md already specify the job/event/trigger tables this page proposes to widen — nothing here contradicts them; it renames job_id to an object reference so runs are one object kind among seven.