Skip to content
You are reading documentation for unreleased main. Read the 0.1 version.

The Tracker

A company’s work has to live somewhere. Crewlet gives you two shapes, chosen with one field:

tracker:
backend: native # the default — the engine is the tracker
# backend: jira # the tracker is Jira, and the engine mirrors none of it

The two are not variations on one design. They are opposite answers to the same question, and each is coherent on its own terms.

Every change to the company’s work is one record on an ordered log the whole fleet shares, and every node derives the same SQL tables from that log — see the log and the copies. There is a board on the dashboard, tools a seat calls, and an MCP surface your own AI assistant can reach.

This is not a mirror. The log here is the source of truth, so the staleness argument below does not apply to it: there is no other copy to disagree with, no webhook to miss, and no reconciliation poller because nothing is being reconciled. A node’s own tables can be behind the log, and that is handled by saying so — every answer carries how far this node has applied and what it could not account for, and a read that cannot be served refuses rather than answering “there is no such item”, because the second is an answer somebody acts on.

What it is deliberately not is a Jira. There is no workflow engine and no permission scheme, and nothing has to be configured before a company can file its first ticket: declaring a unit with a project key in the config creates the project, on every node, with no gesture from anybody.

What it does carry is what an agent company actually uses — a key, a type from a per-project catalogue, a status from a closed set of six in four groups, an assignee, a thread, a history, subtasks, tags, typed custom fields, saved views in five shapes, and a project’s files — whose rows are the tracker’s and whose bytes the object store keeps in one store the whole fleet shares rather than in every data node’s database. The line is between structure a company records and process a tool enforces: the first is here, the second is not. There is no gate that refuses a transition, no scheme that hides a field from a role, and no setup form standing between a founder and their first task.

The whole surface is in The Work Tracker.

Because the alternative — the paragraph below — costs a founder an Atlassian site, a project, six service accounts and a webhook before their company can record that it did anything. That is a real barrier for the case Crewlet is for, and the vendor path stays fully supported for the companies that are already on it.

Task lifecycle lives entirely in Jira, and the engine mirrors none of it: no task table, no status field, no assignee map, no dependency graph, no reconciliation poller. A ticket’s state is whatever Jira says it is, read live through an agent’s own MCP tools.

That is the design, not a gap. A mirror of somebody else’s task state is a cache with no invalidation story: every webhook you miss, every edit made in the PM tool’s own UI, and every retry that arrives out of order leaves the engine confidently wrong about work a person can see is finished. Keeping nothing means there is nothing to be stale.

Jira is the one external tracker the field names. Work kept in GitHub or GitLab issues needs no tracker setting at all: the GitHub and GitLab integrations route their issue webhooks to seats whatever tracker.backend says, and agents act on issues through those vendors’ MCP tools — beside the engine’s own tracker, or with tracker.backend: none, where the engine runs no tracker of its own.

Everything below this line is the same decision on either backend: routing, who assigns, hand-offs, the lead fallback, a change becoming a turn. A unit’s project key names its project on whichever tracker the company runs, which is why the field is not called jira_project.

What differs is only the write that carries a decision and the signal that follows it. On native a seat writes through the engine’s own tools, and the committed record is what wakes the next seat. On jira it writes through Jira’s tools over MCP, and Jira’s webhook is what wakes the next seat.

On the native backend a write does not go to a node’s database. It is published as a record onto the domain’s own log, on the subject of the object it changes — one task, one project, one saved view — and the broker arbitrates: two writers racing on one task contend there and exactly one wins, while two writers on different tasks never contend at all. Every node then consumes that log in order and applies the same records into its own SQL tables, committing the rows and its position on the log in one transaction. There is no node whose copy is the real one and no leader, and every node arrives at the same rows because they all replay the same order.

What that shape buys is that a copy can say exactly how far along it is, which a cache cannot:

  • Every answer carries the level it was actually served at, never the one the caller asked for. A read never silently downgrades, so the two can differ only by a refusal you can see.
  • A write’s outcome has three values, not two. applied means the record is durable and in this node’s rows. pending means it is durable and this node has not consumed it yet — which is a fact about this node, not a failed write, and never something to retry. unknown means the acknowledgement was lost and the record may or may not be there; that is the one worth retrying, and the reply carries the operation id to retry it with, which collapses a duplicate rather than filing one. The id carries the instant it was minted, so a retry — a turn re-run included — is judged by when the operation began rather than when it was retried: on a node whose record of what already landed may have lost that operation’s row since — to its thirty-day sweep — it answers unknown rather than applying it twice (see Replication). A gesture that writes several records in order — a cross-project move, a merge, a promotion, a dependency change, a subtree’s removal or restore, a write that declares its labels first (labels_create_missing), an update that changes the item and its dependencies — stops at the first one whose outcome is unknown rather than carrying on over it: nothing after that step is written, a mid-move or mid-merge mark stays up, and the caller is told the gesture stopped and under which operation id. Running it again under that id — a seat by repeating the call, the operator’s assistant by sending back the op_id its answer carried — answers the steps that landed and finishes the rest. The exception is a step this node’s ledger cannot vouch for: the answer then says so, because running it again here stops at the same step. A create in that position answers from its own item’s row instead: the item, where this node holds it, and unknown with no key minted where it does not.
  • A write can wait for itself. A turn that files a task and then lists the project sees what it just filed, because the tool waits for this node to apply its own position before it reads. A gesture that writes one item twice waits the same way: a dependency change naming both directions records the edges this item waits on and the ones that now wait for it as two commits on the same item, and the second carries the first’s position — so it decides from a state that contains it rather than losing a race with itself. When this node is too far behind for that wait to finish, the refusal names your own write and the position to retry against — a different situation from a colleague editing the same item, and it reads differently.
  • An answer that could not account for everything says so. A node holding a record a newer build wrote — one this build cannot decode — reports the answer as incomplete, names how many records and which objects, and the board renders that above the rows. It is a different fact from staleness, and a screen that showed only staleness would look confidently right.
  • A node claims no new seats until every domain that gates admission is established. A seat whose tools read incomplete tables would answer “there is no such task” and act on it, by filing the duplicate or telling a person their link is dead. The node keeps every seat it already holds — catching up is not a reason to drop work in hand — and the fleet view reports how many of its copies are ready.

A node too far behind to catch up adopts a peer’s snapshot. The log does not keep records for ever (see below), so a node that was down long enough, or that has never run, can be below the oldest record the log still holds — and there is nothing left for it to replay. At boot it asks the fleet, verifies what it is offered against its own requirements and checksum, and installs it wholesale before anything reads from it. A company with no peer able to donate starts anyway, on the history it has, and says what it cannot account for.

Retention is the one asymmetry worth knowing: tasks, comments and pages are kept for ever — a tracker that forgot would stop answering the question it exists for — while a domain’s log keeps only the replay window, which is bounded by durability rather than by age: a record is trimmed once every node has applied past it, a complete backup covers it, and it is at least a week old. A company that never backs up never trims, deliberately.


A change is a change, whoever recorded it: a Jira webhook and a native item’s own change record both arrive at the notification service as a delivery and both become an ExternalNotification. Nothing above that seam knows which.

PM-tool webhooks do not become dedicated task events. Every webhook is parsed by the notification service into an external_notification delivered to the routed agents’ inboxes: the assignee, watchers, @-mentioned agents, or the project lead as a fallback (see Jira Integration). The woken agent then acts on the item through the tracker’s own tools: the engine’s builtins (update_work_item, comment_on_work_item and the rest) on native, and its MCP tools on Jira.

EnginePM tool (Jira/GitLab)EnginePM tool (Jira/GitLab)the notification service parses + routes →external_notification to the project lead's inbox(fallback routing) → lead agent turn→ external_notification to the assignee's inbox → agent turn→ external_notification to watchers, assignee,and @-mentioned agentsAgent creates subtask, transitions ticket, postscomment — all through MCP tools, same as a human wouldTicket created (webhook)Ticket assigned (webhook)Comment added (webhook)MCP tool call

The task_assigned event type (types.TaskAssigned) exists for engine-internal work injection (the Scheduler publishes it to a seat’s inbox for cron-style recurring tasks, internal/schedule/scheduler.go) and never for the PM-tool webhook pipeline, which produces external_notification instead. The two are deliberately different types: one is the engine giving a seat work, the other is the world telling a seat something happened.


See Organization Model for how unit leads and rosters are configured.

Task assignment is not an algorithmic strategy: it is a team lead agent’s reasoning decision, on either backend. When a work item appears, the change reaches the team lead — through the lead fallback when it names nobody else, or because the lead is assigned, watching or mentioned. The lead reasons from the team roster in its prompt (each direct report’s background, goal and responsibilities) and assigns the item. Only the write differs:

  • native — the lead calls update_work_item with an assignee (or files the item already assigned, with create_work_item), and may add a one-line reason that the new assignee is woken with and the item’s history shows beside the hand-off. The committed record is what wakes the assignee: the wake is derived from the log, so there is no webhook to wait for and none to miss. An agent moving the assignee spends one of the task’s 8 hand-offs, and any touch by a person or an operator returns them all — see Hand-offs are bounded on the task.
  • jira — the lead sets the assignee in Jira through its own MCP tools, and the assignment webhook wakes the assigned agent.

A human can also assign directly — on native from the dashboard, which writes as the person their token is bound to (/operator/act), or through their own assistant over the operator MCP; on jira in Jira itself — and the same agent wakes up. For top-level tasks (no team lead above), the founder assigns directly, or a C-level agent role acts as the top-level assigner.


There is no special escalation mechanism in Crewlet. When an agent is blocked or out of its depth, it hands off the same way a human would:

  • The agent reaches its manager from the executor with the colleague-surface tool that fits where the work lives: a comment on the work item (comment_on_work_item on native, a Jira comment on jira), a Slack mention, or a2a_ask for tight-loop sync. If the blocker only becomes clear at review, the reviewer returns self_iterate with a note saying so, and the executor’s next round makes the outreach. Once the handoff has been made, the reviewer ends the turn done: the manager’s reply is what re-triggers the agent, so no further round of that turn can produce it.
  • The getting-unstuck tool skill (see examples/tool-skills/getting-unstuck.md) teaches the agent the discipline: include what you tried, options you see, your recommendation, and urgency. Never hand a naked problem.
  • The agent’s identity prompt names its manager, so the handoff target is always resolvable.

When the engine itself stops a turn it ends it as failed and publishes the cause: turn.guard_breach for a fired guard (stall, max-iteration exhaustion, the delegation-depth cap, a scheduled turn’s wall-clock cap), budget_exhausted for a spent token budget, or llm_unavailable for an exhausted provider chain. The failure is kept on the seat as its last_error, with a cause-specific status line, so the founder sees what happened; an exhausted provider chain also reads the seat as stopped with the reason provider until it works again, and a spent budget as stopped/budget while its window refuses (see Agent States).


Native trackerA person files 'Build auth API' in project AUTHfrom the dashboard, with no assignee

Enginethe committed record names nobody else,so the lead fallback wakes the team lead

Team lead reads the task, queries knowledge,and assigns it to itself with update_work_item

Team lead files subtasks with create_work_item, each assigned:'Design auth endpoints' → Senior Engineer'Implement JWT middleware' → Senior Engineer'Write auth tests' → Junior Engineer

Each committed subtask wakes its assignee —derived from the log, with no webhook in between

Agents work in parallel, moving status with update_work_item

A subtask reaching done wakes the parent's assignee: the lead

Lead reviews results and moves the parent task to done

PM Tool (Jira)Issue created: 'Build auth API'Webhook fires → the notification service

Engineexternal_notification (project-lead fallback routing)

Team lead's inboxa human lead has no inbox: the delivery to that seat isrecorded as skipped, and the human sees it in the PM tool

Team lead reads task, queries knowledge.Creates subtasks in Jira via MCP tools:'Design auth endpoints' → Senior Engineer'Implement JWT middleware' → Senior Engineer'Write auth tests' → Junior Engineer

Jira webhooks fire for each assignment

external_notification routed to each assignee's inbox

Agents work in parallel, transition tickets via MCP

Transition webhooks → watchers (incl. the lead) notified

Lead reviews results, transitions parent ticket

Transition webhook → manager notified (watcher/mention)

turn.guard_breach, budget_exhausted or llm_unavailable

The dashboard reports the failure on the seat with its cause —stopped when the provider or the budget is why — first inthe overview's attention queue. The founder follows itto the seat, the turn, or the log.

Engine-driven failure (a guard, a spent budget, the LLM down)

Agent (e.g. Junior Engineer)working on task, encounters blocker

The executor calls the colleague-surface tool for the manager(or the reviewer returns self_iterate so the next round makes it),targeting the surface that fits where the work lives.Once it has fired, the reviewer ends the turn done

Colleague-surface tool fires(a work-item comment / slack / jira / confluence / a2a)

Manager sees the mention on the same surface they alreadyuse for human teammates; their next turn fires when they reply

Agent-detected blocker

Part of Crewlet. Generated from crewlet/crewlet main at f665f5a. This is not the current version — see the latest docs.