Skip to content
fosferonPublic

About

A dependency-aware work-coordination engine for Elixir — a work DAG, an allocation tree, and a query language that reports what it withheld. Over SQLite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

90 Commits

Folders and files

Repository files navigation

Bee

A dependency-aware work-coordination engine for Elixir — over SQLite.

Bee models units of work and the relationships between them: a dependency DAG that knows what blocks what, an allocation tree of projects and agents, and a query language that does not just return results but tells you what it withheld and how to ask again. It is designed for programs and agents that need to track, query, and reason about work — not just store it.

  • A real dependency graph. Seven dependency types with distinct meaning — gating (blocks, waits_for, conditional_blocks), non-gating (related, discovered_from, replies_to), and structural (parent_child). Cycle detection happens at write time; ready/0 honours every gating rule.
  • Describe intent, get answers. Bee.ask(:what_next, agent: "mercury", project: "api") is a pre-composed query. Register your own intents (Bee.register_intent/2) and a string name resolves to the same engine — no drift.
  • Queries that report their omissions. Every Bee.query/1 returns the matching issues plus a withheld map (what was truncated or omitted, and why) and a refine hint (how to ask for it). Built for consumers that would otherwise choke on unbounded output.
  • Measurements × dimensions. Capture opaque outside signal — cost, tokens, duration — against arbitrary dimensions (agent, model, branch). Schemaless but indexable. Roll effort up over a tree, a dependency closure, or the critical path.
  • An append-only event log. Every mutation is recorded unconditionally — status changes, comments, blocks, assignments, locks. Bee's autobiography, captured with no opt-in.
  • Two first-class surfaces. The in-process Bee.* API and a versioned GenServer message protocol. A consumer reaching bee across a node is as supported as one calling a function.
  • Honest concurrency. A single writer serialises all mutations (so cycle checks and id allocation are correct); a two-lane reader pool serves reads from WAL snapshots. Durability promises state their conditions — no false absolutes.

Bee persists to its own SQLite database and owns its schema. No consumer creates, alters, or drops objects in it. The boundary is the namespace.


Installation

The Hex package name is bee_tracker; the OTP application remains :bee and the public modules remain Bee.*. This release candidate has not yet been published to Hex. The current master is available from Git:

def deps do
  [
    {:bee, git: "https://github.com/fosferon/bee.git", branch: "master"}
  ]
end

For a reproducible build, pin the reviewed runtime commit with ref: "05ea3a4dcb7b314ce15927fb51554ee81f036816" instead of branch: "master".

After the first Hex release is published, use the package alias:

{:bee, "~> 0.3.0", hex: :bee_tracker}

Then fetch and compile:

mix deps.get
mix deps.compile

Bee requires Elixir 1.19+ and a working SQLite (bundled via exqlite).


Quick start

Bee is a supervised process. Add it to your application's supervision tree:

children = [
  {Bee.Supervisor,
   db_path: "priv/bee.db",
   prefix: "bee",
   jsonl_path: "priv/bee.jsonl"}   # optional — a derived, append-only export
]

Supervisor.start_link(children, strategy: :one_for_one)

prefix namespaces the internal ids (bee-1, bee-2, …) so several bee instances can share analysis tooling without colliding. You address issues by their plain integer — bee resolves the prefix for you.

Create, block, and find ready work

{:ok, auth} = Bee.create("Implement auth", priority: 2, project_id: "api")
# => {:ok, %{id: 1, title: "Implement auth", status: "open", ...}}

{:ok, billing} = Bee.create("Add billing", priority: 3, project_id: "api")
# billing depends on auth — billing is blocked until auth closes
:ok = Bee.block(billing.id, auth.id, type: :blocks)

{:ok, ready} = Bee.ready()
# => {:ok, [%{id: 1, title: "Implement auth", ...}]}   # billing is not ready

{:ok, [^auth | _]} = Bee.critical_path()

Query, and read what was withheld

{:ok, %{issues: issues, withheld: withheld, refine: refine}} =
  Bee.query(status: "open", project_id: "api", limit: 5, detail: :compact)

# withheld => %{limit: 42}            # 42 more issues exist beyond the limit
# refine  => [offset: 5]              # request the next bounded page

Four detail levels shape each result — :minimal | :compact | :standard | :full — so a list view doesn't pay for full descriptions, and a detail view can.

Query limits are capped at 500; page with offset instead of requesting an unbounded result. Queries also compose plain-text search, multiple projects, dependency readiness, and recency ordering in one database operation:

{:ok, %{issues: recent_track_work}} =
  Bee.query(
    text: "FameLine",
    project_ids: ["api", "web"],
    status: nil,
    ready: true,
    order_by: [updated_at: :desc],
    limit: 10,
    detail: :minimal
  )

Ask in your own words

# A built-in intent
{:ok, %{issues: top}} =
  Bee.ask(:what_next, agent: "mercury", project: "api", limit: 3)

# Register your own — a named, stored query spec
:ok = Bee.register_intent("stale-and-open", status: "open", order_by: [created_at: :asc])
{:ok, %{issues: stale}} = Bee.ask("stale-and-open")

# Discover stored vocabulary instead of guessing it
{:ok, intents} = Bee.list_intents()
{:ok, measures} = Bee.list_measures()

Measure, then roll up effort

:ok = Bee.register_measure("effort", "hours", domain: :non_negative)

:ok = Bee.measure(auth.id, %{
  measure: "effort",
  value: 6.0,
  dims: %{"kind" => "estimate", "agent" => "mercury"}
})

# Sum declared effort across the whole subtree under a node
{:ok, rollup} = Bee.rollup(epic.id, scope: :tree, kind: :estimate, meaning: :to_complete)
# => {:ok, %{total: 22.0, covered: 18, missing: 0, ...}}

Rollup scopes: :tree (subtree by parent), :closure (dependency transitive closure), :critical_path (the longest weighted path). If some nodes lack a measurement, bee reports total: nil, a partial_total, and a refine hint listing exactly which ids are missing — it never silently invents a number.


Core concepts

The dependency vocabulary

Not all relationships gate. Bee refuses meaningless dependency types at the boundary — each one carries a distinct meaning:

Type Class Meaning
:blocks gating B can't start until A closes
:waits_for gating B waits for all of A's children
:conditional_blocks gating B runs only if A fails
:parent_child structural parent blocked ⇒ children blocked (derived from parent:)
:related non-gating loose "see also"
:discovered_from non-gating provenance
:replies_to non-gating threading

Only gating edges feed ready/0, critical_path/0, and dependency rollups. Traversal honours the direction you ask for:

{:ok, blockers} = Bee.traverse(id, direction: :blockers)    # why is this gated?
{:ok, dependents} = Bee.traverse(id, direction: :dependents) # what does this gate?

# A bounded blocker-to-goal chain, or a project-scoped global path
{:ok, chain} = Bee.critical_path(root: id, depth: 30)
{:ok, project_path} = Bee.critical_path(project_id: "api", status: "open")

The allocation tree

Alongside the dependency DAG, bee tracks who works on what:

:ok = Bee.register_project("api", name: "API service")
:ok = Bee.register_agent("mercury", name: "Mercury", type: "worker")
:ok = Bee.join_project("mercury", "api")
:ok = Bee.assign(auth.id, "mercury")

{:ok, projects} = Bee.list_projects()          # includes issue_count
{:ok, agents} = Bee.list_agents()

{:ok, load} = Bee.agent_load("mercury")          # how many open issues?
{:ok, pairs} = Bee.who_blocks_whom()              # which agents gate each other?
{:ok, bns} = Bee.bottlenecks()                    # overloaded agents + their projects

Measurements: measures × dimensions

A measure is numeric and calibratable (effort, tokens, cost). A dimension is categorical and groupable (agent, model, branch). Bee never interprets dimension values — it partitions on them, which makes it simultaneously agnostic and useful.

:ok = Bee.register_measure("tokens", "tokens")
:ok = Bee.measure(billing.id, %{
  measure: "tokens",
  value: 1840,
  dims: %{"kind" => "actual", "model" => "claude", "agent" => "mercury"}
})

Every measurement carries a "kind" dimension ("estimate" or "actual") so declared plans can later be measured against what was earned. Dimensions are schemaless JSON but indexable on demand — arbitrary vocabulary, no migration, fast GROUP BY.

The event log

Every mutation — create, update, comment, block, unblock, assign, lock, measure — is written to an append-only events table with a per-issue monotonic sequence. It is bee's autobiography, captured with nobody opting in. Tail it by the global id cursor, or replay an issue's history by sequence.

Locks

{:ok, lock} = Bee.lock(auth.id, locked_by: "mercury", ttl: 30)   # 30-minute hold
{:error, :already_locked} = Bee.lock(auth.id, locked_by: "venus")
:ok = Bee.unlock(auth.id)

Locks carry a TTL and a holder. A background sweeper releases expired locks; each expiry is recorded as an event. force: true takes the lock from its current holder.


Two consumption surfaces

Bee supports two equal, disjoint interfaces:

  1. The Bee.* module API — the in-process facade shown throughout this README. Structural errors raise ArgumentError in your process for good ergonomics.
  2. The Bee.Repo GenServer message protocol — a versioned wire contract for node-crossing consumers ({:create, title, opts}, {:ready, opts}, …). Invalid messages return {:error, reason} and never raise inside the writer — a raise there would kill the single writer and every queued command behind it.

A change safe for one surface is proven safe for the other.


Configuration

Bee.Supervisor accepts:

Option Required Default Notes
:db_path yes — Path to the SQLite database file.
:prefix no "bee" Namespace for internal ids.
:jsonl_path no nil If set, bee maintains a derived JSONL export (debounced, flushed on orderly shutdown).
:name no Bee.Supervisor Supervisor name.
:repo_name no Bee.Repo Repo GenServer name — pass it to the server arg of any Bee.* call.
:pool_name no derived Reader-pool supervisor name.
:sweeper_name no derived Lock-sweeper name.

You can also start a standalone Bee.Repo directly (handy in tests):

{:ok, _pid} =
  Bee.Repo.start_link(db_path: db_path, prefix: "test", jsonl_path: nil, name: :my_bee)

Architecture notes

  • Single writer, reader pool. All writes go through one GenServer over one connection, inside BEGIN IMMEDIATE transactions. Reads are served by a two-lane NimblePool of read-only connections — a :fast lane for point lookups and lists, a :compute lane for traversals, critical paths, and rollups. Heavy compute never blocks list/1. WAL mode gives readers MVCC snapshots for free.
  • Versioned migrations. Schema changes run at boot, in order, behind PRAGMA user_version. No ad-hoc DDL, ever.
  • Honest durability. On an orderly stop or a trapped exit, bee flushes the JSONL export and runs a TRUNCATE checkpoint. On a brutal kill, neither runs — the WAL is recovered on the next boot and at most the last un-flushed event window is missing (re-exportable, since JSONL is derived from the database).
  • Raw SQL, by design. The work DAG needs recursive CTEs, FTS5, expression indexes, and single-writer serialisation that an ORM would obscure. SQL is confined to the storage layer (Bee.Store.*); it never leaks above it.

Import / export

{:ok, count} = Bee.import_jsonl("priv/bee.jsonl")

The JSONL export is one object per issue (with its comments and measurements). On first boot, if the database is empty and jsonl_path exists, bee seeds from it — so the file is both a backup and a portability format.


Testing & development

mix test
mix docs         # build this README into HexDocs
mix hex.build    # build the package tarball

License

MIT — see the LICENSE file.

About

A dependency-aware work-coordination engine for Elixir — a work DAG, an allocation tree, and a query language that reports what it withheld. Over SQLite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages