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 itThe two are not variations on one design. They are opposite answers to the same question, and each is coherent on its own terms.
native — the engine is the tracker
Section titled “native — the engine is the tracker”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.
Why the engine grew one
Section titled “Why the engine grew one”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.
jira — the tracker is somebody else’s
Section titled “jira — the tracker is somebody else’s”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.
What is identical either way
Section titled “What is identical either way”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.
The log and the copies
Section titled “The log and the copies”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.
appliedmeans the record is durable and in this node’s rows.pendingmeans 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.unknownmeans 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 answersunknownrather 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 isunknownrather 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 theop_idits 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, andunknownwith 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.
How it works with webhooks
Section titled “How it works with webhooks”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.
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.
Assignment: Team Lead as Decision Maker
Section titled “Assignment: Team Lead as Decision Maker”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 callsupdate_work_itemwith anassignee(or files the item already assigned, withcreate_work_item), and may add a one-linereasonthat 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.
Manager handoffs (no special escalation)
Section titled “Manager handoffs (no special escalation)”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_itemonnative, a Jira comment onjira), a Slack mention, ora2a_askfor tight-loop sync. If the blocker only becomes clear at review, the reviewer returnsself_iteratewith a note saying so, and the executor’s next round makes the outreach. Once the handoff has been made, the reviewer ends the turndone: the manager’s reply is what re-triggers the agent, so no further round of that turn can produce it. - The
getting-unstucktool skill (seeexamples/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).
Data Flow Examples
Section titled “Data Flow Examples”Task Created on the Native Tracker
Section titled “Task Created on the Native Tracker”Task Created in Jira
Section titled “Task Created in Jira”Manager-handoff Flow
Section titled “Manager-handoff Flow”Part of Crewlet. Generated from crewlet/crewlet main at f665f5a. This is not the current version — see the latest docs.