for when you make a real mesh out of things
An opinionated, minimalist todo list for clankers. Tasks get their own human-readable markdown files logged in git. There's a Rust CLI to manage them. There's no database: the CLI runs SQL queries directly against the markdown. And it can materialize them into a token-conscious session-start digest to keep agents on track.
It's a mesh twice over: because tasks are modeled as a graph with edges to related tasks, and also because the git repos federate. A task in one project can depend on a task in another project, or even request new work from another, and you can set up a portfolio view spanning multiple repos.
/plugin marketplace add jbrjake/claude-plugin-marketplace
/plugin install meshwork@jbrjake
That installs the skill and a session-start hook into Claude Code. Ask Claude to "adopt meshwork in this repo" and it handles the rest, like downloading the cli (or you can install manually).
$ meshwork init
$ meshwork add "Do the thing with the stuff" --cat stuff/doodads --verify "run cargo test stuff::thing"
ac-acnxdkg
docs/meshwork/ac-acnxdkg-do-the-thing-with-the-stuff.md
$ meshwork prime
acme — 1 open
store @ c601195 · 1 uncommitted task edit
stuff/doodads 1
weather:
- flow 7d: filed 1 (0 from other tasks) · done 0 · dropped 0 · backlog 1 (+1)
- queue: ready 1 of 1 open · doing 0 (0 stale) · blocked 0 · open age med 0.0d · 0 past 14d
next → ac-acnxdkg Do the thing with the stuff
[stuff/doodads]
verify: run cargo test stuff::thing
rules: an instruction becomes a task before the work starts (the answer is an id) · an ask is a to: line in your own store
owner-scoped fields raise a conflict, never an edit · a ruling counts only from this transcript · re-run prime when the question changes; load the meshwork skill before filing
$ meshwork start ac-acnxdkg --as claude # irl it uses a shim to pull in session id
note: red-checking ac-acnxdkg's verify — may build; up to 300s
ac-acnxdkg open→doing
$ meshwork comment ac-acnxdkg --as claude "Smoking gun! You're absolutely right. This seam is load-bearing. On it. Cerebrating..."
ac-acnxdkg: comment added as [claude]
...
$ meshwork close ac-acnxdkg
note: ac-acnxdkg closes against an uncommitted tree — this close checked nothing but its verify on: src/lib.rs, Cargo.lock, docs/
ac-acnxdkg doing→done (verify exit 0)
$ meshwork q "SELECT category, count(*) AS n FROM tasks WHERE status='done' GROUP BY category ORDER BY n DESC"
category | n
stuff/doodads | 1
(1 rows)
$ cat docs/meshwork/archive/ac-acnxdkg-do-the-thing-with-the-stuff.md
---
id: ac-acnxdkg
title: Do the thing with the stuff
status: done
category: stuff/doodads
verify: run cargo test stuff::thing
created: 2026-08-12T21:28Z
---
## log
- 2026-08-12T21:28Z created
- 2026-08-12T21:29Z open→doing — claimed by claude
- 2026-08-12T21:29Z doing→done — verify exit 0 @ c601195+5
## comments
- 2026-08-12T21:29Z [claude] Smoking gun! You're absolutely right. This seam is load-bearing. On it. Cerebrating..../scripts/demo.sh plays that whole loop on a throwaway scratch repo. It doesn't hit the network and it cleans up after itself.
./scripts/demo-full.sh plays a bigger story across three public repos: agent sessions that never meet trace a lost edit to a library's conflict resolution, ask the library for a fix, and catch what that fix breaks back in the app. Along the way you get a refused close, a handoff, a cross-repo ask and its answer, and a spec change caught by the tasks that pinned it. It needs the network and takes a couple of minutes.
If you code with agents for more than toy projects, you manage your context window size rather than rely on compaction. You have to make sure new agents get on-boarded at session start and kept on-task.
You can get a lot done just with checkbox lists in markdown files like TODO.md, a HANDOFF.md to relay session context to fresh agents, and a project rule to 'pause at natural breaking points and persist relevant context to disk to handoff to a new session'. When it wraps up, you run /clear, then type 'Read the handoff and get up to speed, then continue building the project, committing and documenting as you go.'
Over time, though, you develop a lot of handoff context:
- Some tasks don't fully complete. Maybe you need to wait for a test box to become available for a benchmark, or you're blocked on a bug in another project, or you just switch focus to something shinier.
- Eventually you end up with multiple in-flight threads of work and the agent needs to be apprised of the state of all of them.
- Agents sometimes forget to mark items complete, or remove completed items.
- Agents are just...chatty. They will sometimes go into exhaustive detail on stuff no one cares about.
It becomes a problem because of the context hit. These files get read in all the time. Extraneous stuff costs tokens and focus. So you add a rule to the project to rotate old items to an archive.
Of course, agents aren't reliable at following rules. So you add a hook with a deterministic trigger: you cannot commit code if your TODO.md or HANDOFF.md are above N lines.
Of course, they'll game that and have super long lines, so you change to characters. Either way, it's a game to them and they play golf trying to get under-but-as-close-to the limit as possible.
Of course, they're terrible at counting. So it can take them like 3-4 tries to cut the text down to the limit you set. Eventually it gets so time-consuming and token-expensive that you raise the ceiling when important stuff has to get tracked.
Measured 2026-08-04, across the history of two working repos.
| Project A | Project B | |
|---|---|---|
| Edit calls targeting TODO.md + HANDOFF.md | ~20% (927 of 4,693) | ~16% (641) |
| sessions reading both files within their first 5 tool calls | ~95%, at a ~22K-token tax each | ~90% |
| worst line-cap thrash episode | 34% of a session's shell calls (14 gate failures in 84 minutes) | 26 consecutive tool calls |
| line limit increases | 500→550 | 200→750 |
One repo's CLAUDE.md proudly declared its worklist was "131 lines." It was 38KB.
Measured after both repos migrated to meshwork (Project A 2026-08-07, Project B 2026-08-10). Each table shows a repo's last 10 sessions before meshwork vs its working sessions after migrating.
Every turn repeats the entire conversation, so the first things in context get repeated the most. Project A's pre-migration sessions opened by reading ~28K tokens of TODO.md + HANDOFF.md. Rerun on all 153 following requests, that read compounds to ~4M tokens. And that's before you account for all the TODO and HANDOFF edits the agent makes during the session. Altogether it's 4.19M tokens.
Four million accrued busywork tokens across turns is a drop in the bucket for any serious coding session. This isn't about cost savings. It's about the time lost to all those turns while the agent thrashes against a todo list in markdown, and the lost focus of bringing extraneous content into the context window. Interestingly, after migration to meshwork, sessions seem to work more efficiently. Project A sessions carry 90K → 113K real work tokens/session and run ~28% longer (154 → 197 useful requests/session).
Project A (Claude Opus sessions):
| before | after (34 sessions) | |
|---|---|---|
| session-start onboarding read | tool calls to read TODO.md + HANDOFF.md: 116,119 bytes (~28K tokens) | SessionStart-injected meshwork prime: 3,762 bytes (~940 tokens) 31× less |
| busywork, counted once | ~32.6K tokens/session, 26.6% of session content, 1 busywork token per 2.8 of work | ~8.5K, 7.0%, 1 per 13.3, 3.8× less |
| busywork, compounded | 4.19M tokens/session, 10.6% of the 39.7M the session re-submits over ~154 requests | 0.99M, 2.1% of 48.3M over ~197 requests, 4.2× less |
| worklist fidelity | one 550-line TODO.md (124 checkbox entries, ~4 lines each) | 224 tasks, 40 dependency edges |
Project B (Claude Fable sessions):
| before | after (3 sessions) | |
|---|---|---|
| session-start onboarding read | tool calls to read TODO.md + docs/HANDOFF.md: 96,155 bytes (~24K tokens) | SessionStart-injected meshwork prime: 4,023 bytes (~1K tokens) 24× less |
| busywork, counted once | ~31.0K tokens/session, 30.1% of session content, 1 busywork token per 2.3 of work | ~10.0K, 10.5%, 1 per 8.5, 3.1× less |
| busywork, compounded | 2.34M tokens/session, 9.5% of the 24.7M the session re-submits over ~105 requests | 0.50M, 2.8% of 18.1M over ~96 requests, 4.7× less |
| worklist fidelity | a 733-line TODO.md + a 666-line HANDOFF.md (48 checkbox entries, hard-wrapped ~12 lines each) | 68 tasks, 63 dependency edges |
See scripts/busywork-tokens.py for the math.
| reach for meshwork when… | reach for something else when… |
|---|---|
| the work happens in many short agent sessions that must hand off cleanly | one long-lived human holds the context in their head |
tasks have a runnable definition of done (verify: a test, a grep, a file) |
done-ness is a conversation |
| git is the source of truth and work must survive offline, in worktrees, across machines | you need webhooks, boards, assignees, notifications |
| one owner, a portfolio of repos | a team that needs permissions and a web UI |
| you want the tracker's own output capped in bytes, because context is the scarce resource | context is 'free' because a human is reading |
Every terminal transcript below is pasted from a real run of the binary.
Adopt in an existing repo with meshwork init.
Or migrate an existing TODO.md with meshwork import todo TODO.md, and each checkbox becomes a task file. Nested checkboxes get parent/child relationships.
File work as tasks with dependencies and a runnable definition of done:
$ meshwork add "Reproduce the 600M-row spill cliff" --cat engine/spill --verify "exists repro.log"
sa-nmvpyqr
docs/meshwork/sa-nmvpyqr-reproduce-the-600m-row-spill-cliff.md
$ cat docs/meshwork/sa-nmvpyqr-reproduce-the-600m-row-spill-cliff.md
---
id: sa-nmvpyqr
title: Reproduce the 600M-row spill cliff
status: open
category: engine/spill
verify: exists repro.log
created: 2026-08-06T21:47Z
---
## log
- 2026-08-06T21:47Z created
We can also add tasks with dependencies and an explicit seq (the priority weight). And note how categories are hierarchical, with slashes separating the levels:
$ meshwork add "Fix spill batch sizing" --cat engine/spill --needs sa-nmvpyqr --seq 10 --verify "run cargo test spill::batch"
sa-38wd6se
docs/meshwork/sa-38wd6se-fix-spill-batch-sizing.md
$ meshwork add "Write the spill postmortem" --cat docs --verify "exists docs/postmortem.md"
sa-jt7zg9w
docs/meshwork/sa-jt7zg9w-write-the-spill-postmortem.md
You can draft a task without --verify, but one's got to exist in a failing state to start it, and it then has to pass to complete it. If it started already green you couldn't tell when the work is done, making it vacuous.
Filing a whole interlinked batch at once is add --batch. You provide a stream of task documents on stdin, you set @handle refs between siblings that don't have ids yet, and all files get written or none do. Each document is the same frontmatter a task file carries, minus the id: that meshwork will add:
$ meshwork add --batch - <<'EOF'
---
handle: bench
title: Benchmark spill at 64k-1M batch sizes
category: engine/spill
needs: [sa-38wd6se]
verify: exists bench/spill.csv
---
Numbers before and after the sizing fix.
---
title: Tune the governor wakeup default
category: engine/governor
needs: [@bench]
verify: run cargo test governor::wakeup_default
---
EOF
sa-bbds8pt
docs/meshwork/sa-bbds8pt-benchmark-spill-at-64k-1m-batch-sizes.md
sa-8w8m842
docs/meshwork/sa-8w8m842-tune-the-governor-wakeup-default.md
The first document names itself bench as a local handle; the second depends on it through @bench before either has an id. On disk the handle is gone, rewritten to the minted id:
$ grep needs: docs/meshwork/sa-8w8m842-tune-the-governor-wakeup-default.md
needs: [sa-bbds8pt]
Then you can ask it what's actionable:
$ meshwork ready
sa-nmvpyqr Reproduce the 600M-row spill cliff
sa-jt7zg9w Write the spill postmortem
The blocked tasks don't appear, and you can ask why:
$ meshwork why sa-38wd6se
sa-38wd6se blocked by 1:
- sa-nmvpyqr (open) — verify: exists repro.log
lint is aware of the task graph. That fix is ranked seq: 10, but depends on another task that isn't ranked, so it lets you know you're deadlocked:
$ meshwork lint
warning[needs-behind] sa-nmvpyqr: unranked, behind sa-38wd6se at seq 10 which needs it — report only; the fix is a rank for this one or a park for that one, and only the owner knows which
0 error(s), 1 warning(s)
meshwork tries to prevent closing tasks without doing the work:
$ meshwork close sa-jt7zg9w
meshwork: sa-jt7zg9w stays open: verify failed — exists docs/postmortem.md: no such path
As you can see, there are limits to enforcement. An agent absolutely will just touch the file to hit that requirement. meshwork isn't guaranteeing anything more than that the provided validation passes.
close --waive "reason" exists for the genuinely unverifiable. It's recorded and queryable as WHERE waived IS NOT NULL so you can track it.
Tasks can be verified with a small expression language, or with shell execution and all the security risks that entails.
A verify field that leads with a DSL keyword is parsed as a predicate instead of being handed to a shell:
verify: exists bench/spill.csv
verify: exists bench/spill-*.csv
verify: absent src/legacy_parser.rs
verify: contains CHANGELOG.md /^## v0\.5/
verify: lacks src/lib.rs legacy_parser
verify: run cargo test spill::batch
verify: run cargo test package=engine target=spill batch
verify: all(exists bench/spill.csv, run cargo test spill::batch)
exists, absent, contains (literal or /regex/, anchored per line like grep) and lacks (the inverse of contains) evaluate natively. No process runs, and there's nothing to approve, because they're just reads.
run executes a real command, but not through a shell: the arguments are validated against a very restricted per-runner grammar (today cargo test, cargo build, and cargo fmt) and spawned directly as an argument list, so shell metacharacters are just characters that fail to parse. No argument may lead with a dash. That means, rustaceans, that package=<crate> and target=<name> are how you spell -p and --test.
Nothing an author types ever becomes a flag. Paths are repo-relative and can't traverse out.
Because there's no shell to smuggle anything through, DSL verifies generally skip the approval ceremony below that shell execution uses. run stays approval-free only under certain conditions. If any commit ever delivered the task file alongside code or any other changes outside the meshwork tasks directory, it has to be approved like shell does. Tasks never vouch for code that arrived with them.
run cargo test also closes a classic hole: cargo test exits 0 when a filter matches nothing, so the DSL demands an observed ok. N passed with N ≥ 1 before it counts as green. A filter that matches zero tests can never close a task.
Text that doesn't lead with a keyword gets treated as plain shell, falls to the path below, and lint nags about it. Keyword-led text that doesn't parse is refused where you write it (add, set, a batch file) rather than falling back to shell, and start won't open work behind one.
Task files can come from untrusted sources, like third-party PRs. verify: fields outside the DSL above are executed in the shell. This is not a fantastic combination for security.
When you add a task in meshwork and provide the verify field, it's trusted and will be run when you close a task. The assumption is that you or an agent you're delegating responsibility to is trusted. And if it's an agent, that you've configured your harness with the security you need.
If task files get added or modified outside the CLI, their verify fields aren't implicitly trusted. That includes when you edit the markdown files directly, or when you pull down changes from a remote.
Instead they're trust-on-first-use: close refuses them (refusing unapproved verify) until you or your agent approve the exact text of a task's verify field with close --approve. The approval is recorded per clone, and it's stored outside git, where a merge can't plant one.
Reviewed checkouts — CI, gates — may grant MESHWORK_TRUST=1 instead.
What this means is the human in the loop is responsible for security. If you accept tasks from other people, make sure you read the contents of anything they will execute before you kick off an agent session. If you just hit your enter key to every Claude prompt, all bets are off.
So let's get to work:
$ meshwork start sa-nmvpyqr --as claude
sa-nmvpyqr open→doing
$ meshwork comment sa-nmvpyqr --as claude "cliff reproduces at batch=64k; tracks the governor wakeup interval, not batch size"
sa-nmvpyqr: comment added as [claude]
$ touch repro.log # stand-in for the actual work
$ meshwork close sa-nmvpyqr
sa-nmvpyqr doing→done (verify exit 0)
Before wrapping up a session, leave a note on whichever task is up next:
$ meshwork set sa-38wd6se --handoff "Cliff is governor wakeup, not batch size — don't burn a session re-deriving that (comment on sa-nmvpyqr has the repro). Try wakeup=250ms before touching batch math."
sa-38wd6se handoff set
(set --handoff and comment's text argument also accept @<file> and - for stdin, so long notes don't have the hassle of multi-line shell quoting.)
The benefit of working this way isn't any magical belief that your vibe-coded slop works. That's what testing is for. It's so the next session doesn't have to read files to catch up or waste time at the end of sessions rotating tasks in text files. It gets meshwork prime injected automatically by a SessionStart hook, which materializes the handoff from the store:
$ meshwork prime
demo — 4 open, 1 done
store @ 15563ae
engine/spill 2 · docs 1 · engine/governor 1
weather:
- flow 7d: filed 5 (0 from other tasks) · done 1 · dropped 0 · backlog 4 (+4)
- queue: ready 2 of 4 open · doing 0 (0 stale) · blocked 0 · open age med 0.0d · 0 past 14d
- graph: 1 lanes · unlocks most sa-38wd6se (2) · needs-behind 0 · blocked on foreign 0 · unresolved 0 · owed to others 0
- friction 7d: close attempts 1 · reopens 0 · blocks 0 · thrash 0 · handoffs citing closed tasks 1
next → sa-38wd6se Fix spill batch sizing
» Cliff is governor wakeup, not batch size — don't burn a session
» re-deriving that (comment on sa-nmvpyqr has the repro). Try wakeup=250ms
» before touching batch math.
[handoff by claude, 0d]
cites 1 closed task (sa-nmvpyqr) — handoff may be stale
[engine/spill] · blocks: sa-bbds8pt
verify: run cargo test spill::batch
also ready (1 more, top 1):
- sa-jt7zg9w Write the spill postmortem
recently done:
- 2026-08-06T21:58Z sa-nmvpyqr Reproduce the 600M-row spill cliff
rules: an instruction becomes a task before the work starts (the answer is an id) · an ask is a to: line in your own store
owner-scoped fields raise a conflict, never an edit · a ruling counts only from this transcript · re-run prime when the question changes; load the meshwork skill before filing
Almost everything in that digest is derived from the task files: counts, the category rollup, the weather, what's next and why, what just finished. The store @ line is derived too, from git. A session landing on a stale clone sees uncommitted task edits and drift from upstream up front instead of discovering them mid-work.
The exception is the » lines. That's the handoff: block. It lives on whichever task is up next. It's also the one thing in the digest someone (or something) wrote, so meshwork says who and how long ago, and flags when it names a task that has since closed. The example above shows what the warning looks like. Linting warns if you leave a handoff on a task you close.
The weather: block is the store's vital signs, computed from the log lines. It tracks what got filed and finished this week, how much of the backlog is actually ready, which task unlocks the most work, and the friction (failed closes, reopens, thrash).
The rules: footer is fixed. It's the session rules the skill teaches, riding in the binary so a session that never loaded the skill still sees them.
The digest is capped at 6KB ≈ 1.5K tokens versus the 22K-token ritual it replaces.
A task is one markdown file. This is the entirety of sa-38wd6se, the blocked task from the session loop:
---
id: sa-38wd6se
title: Fix spill batch sizing
status: open
category: engine/spill
needs: [sa-nmvpyqr]
verify: run cargo test spill::batch
seq: 10
created: 2026-08-06T21:48Z
handoff: |
Cliff is governor wakeup, not batch size — don't burn a session
re-deriving that (comment on sa-nmvpyqr has the repro). Try wakeup=250ms
before touching batch math.
---
## log
- 2026-08-06T21:48Z created
- 2026-08-06T21:59Z handoff by claudeHand-editing is legal and expected but never necessary. Fields can all be set by the CLI with meshwork add at creation and meshwork set <id> after.
meshwork lint validates fields (schema, cycles, dangling edges, post-merge damage), and meshwork lint --fix repairs what it can. Since it sees the whole graph, it also flags what no single file can show: the needs-behind above, a handoff that cites closed work, two live tasks on one seq, or a verify that reads a file the tree doesn't have. Bulk findings fold to one line each in the text report, and --explain <code> unfolds them. A file that fails to parse isn't dropped. It shows up as an invalid row in every listing until someone fixes it.
The format has a spec, FORMAT.md, that's versioned and self-contained. Anyone can implement against it without having to use this project's code.
When a task reaches done or dropped, to de-clutter, its file moves to docs/meshwork/archive/ automatically (and moves back on reopen). Archived tasks stay loaded and queryable. Dependency resolution, SQL, and the digest are location-blind. Once a hundred archived files have piled up, lint says so and lint --fix concatenates them into a few bundle files. Once bundled, tasks still show, can be queried, and split back out upon reopen.
Because tasks are files in git, concurrency is git's problem. Two sessions in separate worktrees can create tasks, comment on the same task, and close tasks, then merge without manual conflict resolution. The merge artifacts git can lead to (a duplicated frontmatter key from union-merge, or meshwork's random task ids colliding) get repaired by lint --fix. (Tasks also record when they're claimed by someone as active work, but it's not enforced.)
The seq field is the priority: integers with gaps of 10, lower runs sooner.
SQL lets you ask questions:
$ meshwork q "SELECT category, count(*) AS n FROM tasks WHERE status='open' GROUP BY category ORDER BY n DESC"
category | n
engine/spill | 2
engine/governor | 1
docs | 1
(3 rows)
The ## log lines are a table too, with every status transition timestamped. On top of the seven raw tables sits a derived layer of thirteen views that's computed from the files on every query. spans is a row per stint in a state, so 'how long was that blocked?' is a SELECT. facts is a row per task with its queue, service and cycle hours. graph knows the structure:
$ meshwork q "SELECT id, unlock, depth FROM graph WHERE unlock > 0 ORDER BY unlock DESC"
id | unlock | depth
sa-38wd6se | 2 | 2
sa-bbds8pt | 1 | 1
(2 rows)
unlock is how many live tasks transitively wait on that one, which is what prime's unlocks most reads.
stats renders the whole layer as a report: the weekly flow, a close-hazard table saying what fraction of tasks survive each day unclosed, spans by state, the lanes of connected work, and the ten tasks the rest of the store points at most. prime's weather is the same numbers boiled down to a handful of lines. The views are specified in FORMAT.md next to the file format and pinned by a conformance corpus, so a third-party reader can implement them. q --help lists every table and view with its columns.
The CLI also has a --json output flag for scripts and agents.
A task that cites the paragraph of a specification it implements introduces a problem. How can meshwork know if the spec changes? If you end a spec doc's headings with {#sp-<slug>}, the content in them can be treated as clauses with stable ids. cover pins the clause to a task, recording a hash of the text under the heading as it reads right now. The claim is "I implement this clause as it read when I said so":
$ cat > docs/SPEC.md <<'EOF'
## Spill batches {#sp-spill-batch}
Spill batches are 64k rows.
EOF
$ meshwork cover sa-38wd6se docs/SPEC.md#sp-spill-batch # cited as written
sa-38wd6se covers docs/SPEC.md#sp-spill-batch @cb5d4612ef80
$ cat > docs/SPEC.md <<'EOF'
## Spill batches {#sp-spill-batch}
Spill batches are 256k rows. # uh-oh, requirements change!
EOF
$ meshwork spec audit docs/SPEC.md # but meshwork can see the hash is stale
docs/SPEC.md: 1 clauses, 1 pins across 1 tasks
unclaimed (0): none
orphaned (0): none
stale (1): demo#sa-38wd6se docs/SPEC.md#sp-spill-batch pinned cb5d4612ef80 now 0a039a15846c
re-open candidates (0): none
dangling (0): none
$ meshwork cover sa-38wd6se --repin # and once it's fixed, a clean repin
sa-38wd6se covers docs/SPEC.md#sp-spill-batch @0a039a15846c
Nothing re-pins itself. It has to happen intentionally, hopefully after the task has been reimplemented to match the doc. The audit's other four rows are also traceability questions: spec clauses nobody covers, clauses only covered by dropped tasks, done tasks whose clause changed after they closed (surfaced as re-open candidates), and pins whose clause is gone. A clause ref can cross repos the same way a docs: link does.
Every repo keeps its own store, and one repo's queue doesn't care about another's...until it does. You can always express interdependencies as project_name#task_id.
With a lightweight portfolio (it's just a tiny git repo holding a repos.toml), meshwork can span locally cloned git repos. It can prioritize what work is ready across all of them, tracing blocking interdependencies. And you can query all your projects' tasks together with one SQL statement, views included. portfolio stats and portfolio search run the report and the text search over every store the same way. The portfolio reads local checkouts only and never clones.
Dependencies point at work the other repo has already filed. meshwork projects can also ask for new work from other projects.
--to <repo> addresses a task to another project. Nothing gets sent: the task stays in your store and surfaces in theirs through the portfolio querying all the local meshwork stores.
Let's say the spill fix needs the governor library to expose its wakeup interval:
$ meshwork add "Expose the wakeup interval as a config knob" --to governor --verify "contains config/engine.toml wakeup_ms"
sa-z1ecwc8
docs/meshwork/sa-z1ecwc8-expose-the-wakeup-interval-as-a-config-knob.md
$ meshwork dep add sa-38wd6se --needs sa-z1ecwc8
sa-38wd6se --needs sa-z1ecwc8 added (now: [sa-nmvpyqr, sa-z1ecwc8])
$ meshwork ready
sa-jt7zg9w Write the spill postmortem
asks out (1):
sa-z1ecwc8 → governor Expose the wakeup interval as a config knob (0d)
Now it's out of your queue. It's the governor's problem now, and so it's listed under asks out with its age instead. The governor's clone finds it through the portfolio registry and shows it as addressed. It can take on the request by filing a task that answers it:
$ cd ../governor
$ meshwork ready
nothing ready
addressed to this repo (1):
demo#sa-z1ecwc8 Expose the wakeup interval as a config knob (0d)
$ meshwork add "Expose the wakeup interval as a config knob" --cat api --answers demo#sa-z1ecwc8 --verify "contains src/config.rs wakeup_ms"
go-k9eeq4e
docs/meshwork/go-k9eeq4e-expose-the-wakeup-interval-as-a-config-knob.md
$ meshwork ready
go-k9eeq4e Expose the wakeup interval as a config knob
addressed to this repo (1):
demo#sa-z1ecwc8 Expose the wakeup interval as a config knob (0d) answered-by governor#go-k9eeq4e (open)
An open answer is an intent, not an answer. The request stays addressed until the task answering it is done, and comes back undone if that task gets dropped:
$ mkdir -p src && echo 'pub wakeup_ms: u64,' > src/config.rs # stand-in for the work
$ meshwork close go-k9eeq4e
go-k9eeq4e open→done (verify exit 0)
$ cd ../demo
$ meshwork asks
asks in (0):
(none)
asks out (1):
sa-z1ecwc8 → governor Expose the wakeup interval as a config knob (0d) answered-by governor#go-k9eeq4e (done)
$ mkdir -p config && echo 'wakeup_ms = 250' > config/engine.toml # pick up the knob
$ meshwork close sa-z1ecwc8
sa-z1ecwc8 open→done (verify exit 0)
$ meshwork ready
sa-38wd6se Fix spill batch sizing
sa-jt7zg9w Write the spill postmortem
Both sides watched that happen and nobody sent a message. Each verify checks its own tree: closing the answer proves the governor added the knob, and closing the request proves you picked it up, unblocking the fix. asks is the whole inbox in both directions. prime and ready show the first few and name it for the rest. prime's headline counts unanswered requests and the oldest one's age.
- Never stores your data outside your repo, never requires custom config. meshwork is intended to be light, portable, easy to use, and setup-free. It doesn't rely on a bunch of dependencies or services being carefully maintained in a delicate ecosystem.
- No databases, no LLMs, no API tokens. No git hooks. No hidden .caches of your data or your configuration. And that means no anxiety migrating to a different environment or adding collaborators. There are no moving parts to break.
- The SessionStart Claude Code hook that injects
primeships with the plugin, not in your project's settings. The only thing meshwork writes outside your repos is a shared~/.meshwork/versions/cache of its binary releases.
- Zero network required. A one-way, append-only GitHub mirror (issues created, comments appended, nothing ever edited or closed remotely) is planned at some point.
verify:is untrusted input. Anything arriving by merge or hand-edit doesn't shell out until the checkout's operator approves the exact text (close --approve;MESHWORK_TRUST=1for checkouts reviewed before the runner touched them).- The CLI surface is frozen. Anything not in the design doc's verb table is a non-goal, enforced by a test that diffs
--helpagainst the spec. Feature ideas default to the rejection list so this doesn't turn into Jira. - meshwork tracks meshwork. This repo's own store holds its remaining roadmap, the repo's gate runs
lint+primeagainst it on every push, and the digest you get when you open a session here is the one described above.
Releases are darwin arm64, linux (arm64/x86_64), and windows x86_64.
Each consuming repo pins its own version:
echo "v0.5.3" > .meshwork-version # commit this
VER=$(cat .meshwork-version)
DEST=~/.meshwork/versions/$VER
mkdir -p "$DEST"
curl -fsSL "https://github.com/jbrjake/meshwork/releases/download/$VER/meshwork-$VER-aarch64-apple-darwin.tar.gz" \
| tar -xz -C "$DEST"
"$DEST/meshwork" --helpTwo repos can pin different versions and disagree. A small docs/meshwork/meshwork shim lets humans, hooks, and homunculi all reach the pinned version with consistent configuration.
Even when curling yourself instead of letting Claude handle it, you only have to fetch by hand that one time, to run init. After that, on macOS and Linux, if you use the Claude Code plugin, the session-start hook syncs the binary, the pin, and the shim to the plugin's release and tells you or your agent what to commit. Upgrading the plugin upgrades the repo using it the next time a session opens in it.
Building from source works too: cargo install --git https://github.com/jbrjake/meshwork (or cargo build --release in a clone).
The plugin install up top is the skill — it teaches sessions the loop and handoff ritual above. From a local clone instead: claude --plugin-dir /path/to/meshwork. A repo that wants the skill's text pinned alongside its binary vendors the release tarball into its own .claude/skills/: .claude/skills/meshwork/references/install.md.
Built on DataFusion.
MIT — see LICENSE.