Report 2 · Kowobeckett v7.2.0evidence: ~beckett/.beckett as of 16 Aug 2026Aug 14 review → shipped, archived

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.

Prepared for jasonRead-only against the live install (runs.json, dispatch.jsonl, spend.jsonl, channels/, journal/, browser-agent/, memory/, routines.json, beckett.db)Builds on the Aug 14 review and the CTO-seat memory nodes
11
separate stores on disk that are already work-event exhaust (JSON, JSONL, markdown, sqlite)
1,729
rows in events/dispatch.jsonl — the closest thing to a work ledger today (1,144 keyed by run id, 585 by legacy ticket id)
51
runs in runs.json (41 done · 7 parked · 3 cancelled) across 7 repos; 40 carry a PR/commit URL
$813
167 spend rows — every worker stage costed, keyed by run id and session id
294
Discord messages persisted across 14 channel logs, plus 49 browser-agent runs and 69 worker journals
0
rows in beckett.db's event and job tables — the schema is there, the ledger never moved in
0
GitHub webhooks received; GitHub is polled through two cursor files. No Linear, no Sentry, no CI events
11m 16s
from a browser run failing to a fix deployed and reported, with no human in the loop — the episode in Part 2, $3.03
Part 1

One 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.

A data pipeline ends in a warehouse SaaS ingest normalize warehouse dashboard a human reads it. maybe. The Kowo pipeline ends in work — and feeds itself work tools events work state context triggers coworker outcomes every outcome becomes an event; the coworker's own runs are part of the work state it reads next time

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 internal definition
Kowo turns the live exhaust of a company into structured, permissioned work state that AI coworkers can understand, watch, and act on. Beckett is Kowo's first coworker: a software engineer. Kowo notices. Beckett decides and works. Beckett is the go-to-market wedge — nobody wakes up wanting "an operational context pipeline for agentic work"; they wake up wanting an engineer that notices stuff and fixes it. Meanwhile the reusable primitives get refactored downward into Kowo, and the day beckett genuinely depends on Kowo rather than Kowo being a story around beckett, the platform boundary is real.

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?

Part 2

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 run fd4140ea: "Log into X … compose one fresh post". 45 journal events, 42 evals. Two strict mode violation: locator('div[role="dialog"]') resolved to 2 elements errors — 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-evening dispatches browser run 59983c26. 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-fill created. 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…"
11m 16s
second failure observed → fix live and reported
$3.03
implement $2.38 + review $0.66, both sonnet-5
0
human messages in #general between 01:16 and 01:30 (last human message: 23:41 the night before, about babble)
6
stores you have to open to reconstruct this — none of which point at each other except by prose

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)sourceactorverb.objectobject idcorrelationtoday lives in
08-15 16:08:10routinebeckett/routinesroutine.firedx-social-morning→ browser:fd4140earoutines.json state (last fire only, overwritten)
08-15 16:08:10browserbeckett/browser-agentbrowser_run.startedfd4140earoutine:x-social-morningbrowser-agent/…/journal.jsonl dispatched (routine id not recorded; context:false)
08-15 16:09:11 · 16:11:05browserbeckett/browser-agentbrowser_run.errorfd4140ea—journal eval ok:false "strict mode violation … role=dialog resolved to 2"
08-15 16:12:41browserbeckett/browser-agentbrowser_run.finishedfd4140eaartifact: x.com/…/status/2088659…journal finished state:done result:"…"
08-16 01:16:09routinebeckett/routinesroutine.firedx-social-evening→ browser:59983c26routines.json
08-16 01:18:48browserbeckett/browser-agentbrowser_run.finished59983c26artifact: x.com/i/status/2088797…journal finished — the .fill()/retry story is prose in result
08-16 01:19:18runbeckett/conciergerun.createdrun-20260816-fix-flaky-x-compose…caused-by browser:fd4140ea, browser:59983c26runs.json — the two ids appear only inside the prompt text
08-16 01:19:24runwk_0f93bebe (sonnet-5)run.stage.started implementrun-2026…session:df430f8edispatch.jsonl
08-16 01:19:28discordbeckettmessage.postedmsg 1537722… #generalabout: browser:59983c26 (inferred)channels/<id>.jsonl — no run/browser id on the row
08-16 01:24:22runwk_0f93beberun.stage.finished implement · $2.376run-2026…session:df430f8edispatch.jsonl + spend.jsonl (joined on ticketId=run id and sessionId)
08-16 01:28:02runwk_5502559crun.review.passed · $0.657run-2026…session:36b263f8dispatch.jsonl + spend.jsonl + journal/*.log
08-16 01:28:06github0x-beck[bot]change.landedkowo-co/beckett@0e196c3run-2026…runs.json prUrl/published; GitHub itself (not polled into anything)
08-16 01:29:44daemonbeckett/deploydeploy.completed v7.0.12beckettchange:0e196c3 (inferred)uptime.jsonl boot — version not on the row
08-16 01:30:04discordbeckettmessage.postedmsg … #generalabout: 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.

Incident inc:x-compose-fill-2026-08
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 result strings — not by a query
WorkItem wi:fix-flaky-x-compose
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
Run run-20260816-fix-flaky-x-compose-keystrokes-not-fill
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
Change kowo-co/beckett@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
Conversation #general 1520986792373911622 · 01:19–01:30Z
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
Trigger the rule that should have fired
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.

kowo.context.for("incident:x-compose-fill-2026-08") · what the state already supports, assembled by hand from diskcontext pack · illustrative
{
  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 episodeEvidenceThe Kowo primitive that closes it
The two browser runs and the fix run are linked only by ids typed into a promptruns.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 stringsthe observation is beckett's #general message, 10 s after the run was fileda Trigger that is a query over browser_run.error, not a judgment call; the model then decides, it doesn't detect
Nothing remembers the fixgrep -ri "compose\|x.com" memory/ → 0 nodes; the only trace is a chat message and a commitRun.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 runroutines.json keeps only lastFiredAt; browser run's dispatched event carries the task text, not the routine idroutine.fired event with the child id; Trigger→Run edge
The deploy is a boot line with no versionuptime.jsonl {kind:"boot"}; v7.0.12 known only from the chat messagedeploy.completed {version, change} — the event Sentry-style incident matching needs
Cost, stages and transcript are three files with two different keysspend.ticketId holds a run id; dispatch has runId; journal is a filename; sessionId is the only shared key across all threeone event table, job_id foreign key — the schema that already exists in beckett.db
Why this episode matters for the pitch
It is not a demo. It is a routine that runs twice a day on this box, and the box healed its own tooling in eleven minutes for three dollars. What Kowo adds is not the healing — beckett already does that — it is making the healing a record: an incident that can be counted, a trigger a customer can write, a change that a future context pack finds. That is the difference between "we have a clever bot" and "we have a data plane the bot runs on".
Part 3

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 / sizeKowo primitiveWhat it yieldsGradeWhat it takes
events/dispatch.jsonl
src/dispatch/events.ts
1,729 rowsEvent (run.stage.*)every stage transition of every run and deploy, outcome-tagged, timedpresentrename ticketId→job_id (585 legacy rows), add verb.object, insert into event
runs.json
src/run/store.ts
51 runsRun + WorkItem + Changeintent, cast, sessions, blocker/question/proof, PR/commit URL, deps, filespresentsplit: WorkItem = title+prompt+criteria; Run = cast+sessions+cost; Change = branch+prUrl. Mutable → derive from events going forward
spend.jsonl
src/spend.ts
167 rows · $813Event (run.stage.finished cost)per-stage cost, tokens, turns, model, outcome, sessionIdpresentjoin on run id + sessionId; becomes event.cost_usd/tokens. Concierge and browser rows still absent
journal/*.log
src/progress/journal.ts
69 files · 1.6 MBRun.transcript (artifact)tool-by-tool play-by-play per stage; the review verdict textpartialkeyed by filename only; store as artifact ref on the Run, extract the verdict as an event
channels/<id>.jsonl
src/concierge/channel-context.ts
14 ch · 294 msgsConversation + Event (message.posted)author, content, reply-to, mentions, kind user/beckettpartialbounded + 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.jsonl
src/browser/agent.ts
49 runs · 174 MBRun (browser) + Event (browser_run.*)dispatched task, every eval (ok/error/url/ms), finished result, artifactspartialno parent (routine/run/message) id; errors are strings — needs a class field for triggers to count
routines.json
src/routine/store.ts
8 routinesTriggerschedule/watch definitions + lastFiredAtpartialdefinitions are triggers already; fires are overwritten, not logged → emit routine.fired {child}
memory/*.md (+ Moss index)
src/memory/index.ts
14 nodes · 5 typesActor · Project · decisions · calibrationpeople, projects, preferences, typed edges (supersedes/caused-by/about/contradicts/part-of)presentalready the right shape for the curated layer; needs a Run.outcome → memory writeback and to be queryable by object id, not just prose
uptime.jsonlboots/shutdownsEvent (deploy.completed)boot / clean_shutdown timestampspartialadd version + change sha to the boot row
github-prs.json · github-activity.json
src/github/poll.ts · activity.ts
cursors ({} today)Event (pull_request.*, review.*, push)watermarks: seen review/comment ids, last commit sha, last merged PRmissingthe poller sees the events and keeps only the cursor. Persist each observed event; later, a webhook receiver (0 today)
tasks.jsonlegacyWorkItem (v6)ticket tree with branch/PR/publication linkspartialbackfill once as historical WorkItems, then freeze
proposals/0WorkItem (kind product-idea / doctrine)claim, rationale, provenance, decided/becamepresentalready append-shaped; add proposal.decided events
secret-redemptions.jsonl · jingleaudit rowspermissions on Event/Actorwhich entry, which fields (names only), destination kindpartialbecomes the permissions column: what this actor may touch, evidenced by what it did touch
quick/ · free-time/ · concierge-sessions/5 · 1 · 9Run (errand / free-time / conversation session)reports, writebacks, session idspartialsame treatment as runs: an event on start/finish, artifact refs
beckett.db — job, event, node, edge, health, kv0 rows (kv: 2)the destinationevent(job_id, kind, payload, cost_usd, tokens, ts) already indexed by job and kindpartialexists, 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—missingthe 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:

KeyAppears inJoinsVerdict
run id (run-YYYYMMDD-slug)runs.json id · dispatch runId · spend ticketId (yes, that field) · journal filename · worktree pathrun ↔ stages ↔ cost ↔ transcriptpresent — the spine. Field names differ per store
sessionIdruns.json sessionIds.{implement,review} · spend sessionId · concierge-sessionsrun stage ↔ spend row ↔ Claude sessionpresent
channelIdruns.json · channels/<id>.jsonl · progress-cards · concierge-sessionsrun ↔ the channel it was born inpartial — channel, not message; you know where, not which message asked
messageIdchannels rows · task cards—missing as a link from a run/incident back to the message that caused it
browser run idbrowser-agent/<id> · nowhere else—missing — no parent routine, no parent run, no channel; the episode's link is prose
routine idroutines.json only—missing — fires overwrite; children don't know their trigger
prUrl / commit sharuns.json prUrl, published.prUrl · chat textrun → changepartial — the URL is a string; there is no Change record and GitHub state (checks, reviews, merge) is not pulled back
[[wikilink]] namesmemory/*.mdnode ↔ node (typed)present inside memory; missing from memory to any run/change/incident id
The honest read
Two of the seven objects (Run, and Conversation in a lossy form) can be built from disk today with a script; Change is a URL string; WorkItem is the run's prompt; Incident and Trigger-as-fired-event do not exist; Actor and Project exist as memory prose. That is a lot more than zero, and it is exactly the shape of the four extractions in the next section: Events, Work Objects, Context Packs, Triggers.
Part 4

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 existing beckett.db event table with job_id widened to (object_type, object_id) and a correlation JSON 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.landed from 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.emit in src/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.
Gate: 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 existing node/edge tables (typed edges already in RelationType: supersedes · caused-by · about · contradicts · part-of; add resolves and produced).
  • 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 · N browser_run.error with the same class in 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.
Gate: 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 to context.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.
Gate: for a fresh run whose prompt mentions a PR URL, the worker's first three tool calls are not 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) as docs/initiative.md already 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 parked on a typed blocker for > 4 h with no human reply → owed-notification WorkItem · a landed Change with no deploy.completed after 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/watch triggers; they move onto the same table and start emitting routine.fired {child}.
Gate: replay the E1 ledger through the rule set; the "two browser errors" rule fires exactly once, at 2026-08-16T01:18:48Z, producing a WorkItem equivalent to the one beckett wrote by hand. Measured: over 14 days, WorkItems whose origin is a trigger ≥ 1/day, and zero duplicate fires.
Sequencing note
E1 and E2 are a backfill script and a resolver module — a week, sonnet-buildable from a nailed-down spec, no daemon surgery. E3 is where beckett starts to depend on Kowo, and it is deliberately the smallest change with the largest signal (the memory primer already exists; it gets a better input). E4 is where a customer could write a rule. Do them in that order; the Part 2 episode is the fixture for all four gates.
Part 5

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.

Who pays: a 3–30 person engineering team with a real repo and a Discord/Slack, whose lead is already paying for Cursor/Devin/Claude and losing time to the glue.
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.
Risk: it's a services business until the residue is a product. That is fine if every engagement leaves behind the same Kowo node.

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.

Who pays: the same teams as A once they've seen it, plus teams that would never buy a service but will pay to not run a box.
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.
Risk: this is where "context layer for agents" competitors live. Differentiate on local-first, engineering-narrow, and ends-in-work — not on connector count.

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."

Who pays: teams building agents that need to know what's going on; later, other agent vendors.
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.
Risk: launching this first is exactly the "two businesses" mistake. Sequence it last.

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:

the flywheel datasetwhat compounds per 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.

What would falsify this
If, after Part 2 is on the site and five engineering leads have seen the eleven-minute episode, none will pay for lane A — the "install" story is not worth money to the people it's for, and Kowo is a nice architecture with no buyer. Set the number before pitching: N conversations, M paid installs, by a date. Write it in company.md, which is currently a template with every section empty.
Part 6

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: event table widened; kowo backfill over the eleven stores; second-writer hooks in dispatch/spend/channel/browser/routine paths
  • Correlation extractors for ids/URLs in free text
  • beckett events CLI
Acceptance: the fourteen-row Part 2 ledger from one command. Week 1–2. Sonnet-buildable from a spec; no daemon architecture change.

2 · Objects + the first context pack, consumed by the concierge

  • E2 resolvers for Run/WorkItem/Change/Conversation/Incident; haiku error-class classifier
  • E3 context.for assembler; the memory primer takes it as input; workers get it in the brief
  • Measure implement tokensIn before/after
Acceptance: 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
Acceptance: the trigger fires once on replay at 01:18:48Z; the site says what Kowo is in one sentence; one engineering lead has been asked for money and the answer is in company.md. Week 3–4.

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.

Part 7

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.

  1. 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.
  2. 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".
  3. 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.
  4. 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.md is still a template. Answering "what Kowo is" in one paragraph there unblocks beckett's own free-time thinking about it.
  5. Do we keep the events under ~/.beckett or 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.
  6. 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.
Part 8

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.
Appendix

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.

PathWriterShapeAppend-only?
runs.jsonsrc/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/filesmutable, whole-row patch
events/dispatch.jsonlsrc/dispatch/events.ts (DispatchEventBus.emit){ts, runId|ticketId, runRef, branchRef, channel?, stage, outcome, elapsedMs, message?, error?}yes
spend.jsonlsrc/spend.ts{ticketId, project, stage, harness, model, effort, turns, toolCalls, tokensIn, tokensOut, costUsd, durationMs, outcome, reviewTier, ts, sessionId?}yes (O_APPEND + fsync)
journal/<run>.logsrc/progress/journal.tstimestamped lines per stage/workeryes
channels/<channelId>.jsonlsrc/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.jsonsrc/routine/store.ts{routines:[{id, name, action:{kind:schedule|watch,…}, state:{lastFiredAt,…}}]}mutable
memory/**/*.md (+ .moss cache)src/memory/index.tsfrontmatter node (name/type/description/metadata) + body + typed wikilinks; archive/ never deletearchived, never deleted
uptime.jsonlsrc/uptime.ts{kind:boot|clean_shutdown, at}yes
github-prs.json · github-activity.jsonsrc/github/poll.ts · activity.tscursors: seenReviewIds, seenCommentIds, lastCommitSha, lastMergedPrNumbermutable snapshot; no webhooks anywhere in src/github
tasks.jsonsrc/task/store.tslegacy v6 ticket tree with branch/PR/publication linksmutable
proposals/<id>.jsonsrc/proposal/store.tskind, claim, rationale, provenance[], origin, status, decided, becameappend-shaped
secret-redemptions.jsonl · secret-requests.jsonsrc/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/conciergereports, writeback.json, session jsonmixed
identities.json · workspaces.json · access.txt · progress-cards.json · owed-* jsonsrc/discord/* · src/progress/cards.ts · src/concierge/owed-*actor map, channel↔project bindings, access grants, card anchors, owed repliesmutable
beckett.dbschema 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 · kv0 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.

↑ top