Most project tools answer the question “what column is this card in?” Threlmark answers a harder one — “across everything I’m building, what’s the single most important thing to do next, and did the AI agent I handed it to actually ship it?” — and it answers it without a database, without a cloud, and without a single user account. The whole system is a Next.js app sitting on top of plain JSON files on your disk, and the most interesting design decision in it is also the simplest to state: the on-disk layout is the API.

That one choice cascades into everything else — how concurrency is handled, why there’s one file per card, how an external tool like IdeaClyst can participate without permission, and how an AI coding agent can close its own loop and move a card to Done by itself. This is the deep-dive: the decisions and the mechanics behind them, for people who care how the thing is actually built.

Disk is the contract: inside Threlmark’s architecture — ThorstenMeyerAI.com
ThorstenMeyerAI.com
Threlmark · Technical Deep-Dive
Threlmark · architecture

Disk is the contract: inside a local-first roadmap hub

A Next.js app on top of plain JSON files — no database, no cloud, no accounts. The key decision: the on-disk layout IS the API. Everything else cascades from taking that seriously.

Next.js · TypeScript · JSON-on-disk · MIT · part 2 of the Threlmark series
01The core decision

There is no server-of-record — the files are the record

The UI and any external tool reach the same files through the same discipline. The data root defaults to ~/.threlmark — home-based, because it’s a shared hub every one of your apps points at.

~/.threlmark/ ├─ threlmark.json # manifest ├─ links.json # dependency graph ├─ projects// │ ├─ project.json # meta + wipLimits │ ├─ board.json # lane ordering │ ├─ items/.json # ONE card per file ← source of truth │ ├─ suggestions/ # the Inbox (drop-zone) │ ├─ handoffs/ # recorded agent handoffs │ ├─ reports/ # agent report drop-zone │ └─ ROADMAP.md # human-readable mirror ├─ shared/items/ # cards many projects ref └─ archive/ # archived, still readable

Inspectable

Every artifact is a file you can cat, diff, grep, commit.

Portable · no lock-in

Back up with cp, sync with Dropbox / git, migrate trivially.

Interoperable

Any tool in any language joins by reading / writing files.

Restartable

No in-memory state to lose — stateless over the files.

02Making files safe
Amazon

local-first project management tool

As an affiliate, we earn on qualifying purchases.

As an affiliate, we earn on qualifying purchases.

Two disciplined patterns instead of a database

“Just use files” is easy to get wrong. These two patterns — ported from a battle-tested sibling app — are what make file-based state sound rather than reckless.

Pattern 1

Atomic writes

Write to a temp file in the same dir, then rename() over the target. Rename is atomic on one filesystem — a crash mid-write leaves the complete old file or the complete new one, never a half.

write .tmp-pid-rand fsync rename() over target
Pattern 2 · one file per item

The board heals itself

A single roadmap.json array races when two tools write at once. One file per card makes writes collision-free. Lane order lives in board.json and reconciles on read.

The payoff: an external tool never touches board.json. It writes an item file — the board fixes itself on Threlmark’s next read. Unknown keys are preserved, so the contract is forward-compatible.
03Derived, never stored
Amazon

JSON file-based task tracker

As an affiliate, we earn on qualifying purchases.

As an affiliate, we earn on qualifying purchases.

The numbers can’t drift from the files

Anything computable from item state is computed — so the displayed numbers can never disagree with the underlying JSON. Priority is the clearest example: it’s calculated on read, never persisted.

priority — computed on read

Impact weighted heaviest; effort the only axis that subtracts. Reused verbatim from the original tool, so imported cards rank identically.

priority = max(0, round(impact·3 + evidence·2 + fit·2effort·1.5))
a 5 / 5 / 5 / 4 card 29
work-item age
now − lane-entry time. Past threshold (dev 7d, ranked 21d, idea 60d) → stale.
cycle time
first DevelopmentDone. Derived from append-only transitions[].
throughput
items reaching Done per ISO week, 8-week window.
WIP
count per lane; over the cap shows 3 / 2 in red.
04The closed agent loop · press play
Amazon

Next.js development tools

As an affiliate, we earn on qualifying purchases.

As an affiliate, we earn on qualifying purchases.

A handoff is a first-class flow event

The genuinely 2026-shaped part: most building is done by AI agents, so Threlmark closes the loop. Watch a card go from ranked to Done without anyone dragging it.

Handoff → report → self-move

The brief carries a reporting protocol. The agent reports through REST or the filesystem — and a done report moves the card itself.

Ranked
Add price-drop alertsscore 31 · ready
Development
Handed off 🤖
Done
▶ preferred — REST
POST /api/projects/:id/
items/:itemId/report

Direct call. Applied immediately.

▶ fallback — filesystem
drop reports/.json
→ ingested on read

Robust even if the server’s down at finish time.

🤖 claude done: price-drop alerts shipped · typecheck + lint + build passed — card moved to Done
05Portfolio score & deployment
Amazon

AI agent task automation software

As an affiliate, we earn on qualifying purchases.

As an affiliate, we earn on qualifying purchases.

A small formula, and an honest hosting caveat

Because items are globally addressable (/), the Portfolio ranks everything together by a status-weighted score — finishing beats starting, blockers get a boost.

Portfolio ranking — status-weighted

In-flight work floats to the top; bottlenecks cost the most, so blockers get nudged up.

score = priority · statusWeight (+ 0.1 · blockedCount · priority)
1.3
development
1.0
ranked
0.85
idea
0.15
done
Path 1

Static read-only demo

Seeded data, writes to localStorage. Try-before-you-clone.

Path 2

Personal Node instance

Password-gated, persistent backed-up THRELMARK_DATA_DIR.

Path 3

Multi-tenant SaaS

Add accounts + per-tenant isolation. A separate build.

The elegant part: the store interface src/lib/*/store.ts is the natural seam — the same boundary that keeps the local tool simple is the one you’d extend for multi-tenancy. The architecture doesn’t fight that future; it just doesn’t pay for it until you need it.
ThorstenMeyerAI.com
Threlmark · open source (MIT) · github.com/MeyerThorsten/threlmark · part 2 of a series · file layout, formula, weights & agent-loop channels are Threlmark’s actual mechanics.

The problem, stated precisely

If you build many things, your roadmaps fragment. Each project keeps its own list somewhere — a README, a Notion page, a localStorage kanban, a pile of TODOs in a comment block. There’s no single place to ask “what’s the most important thing to do next, anywhere?” and no consistent way to push work into an AI agent and track whether it shipped.

Threlmark generalizes a single-product roadmap — originally a localStorage “Roadmap Lab” kanban with scored feature cards — into a multi-project hub, and then does two things ordinary kanban tools don’t. It makes the data open and portable: the source of truth is JSON on your disk, so other tools can join. And it manages flow, not just status, then closes the loop with the AI agents increasingly doing the building. Everything below follows from taking those two commitments seriously.

Why “disk is the contract”

The central architectural decision is that there is no server-of-record. The UI and any external tool reach the same files through the same discipline; the files are the record. The data root defaults to ~/.threlmark (override with the THRELMARK_DATA_DIR environment variable) — deliberately home-based rather than repo-relative, because it’s a shared hub that every one of your apps points at, not a folder belonging to one project.

The layout is worth reading as a contract rather than a directory listing. At the root sits a manifest (threlmark.json) and a cross-project dependency graph (links.json). Each project gets a folder holding its metadata and WIP limits (project.json), its lane ordering (board.json), and then the important part: one file per roadmap card under items/, a drop-zone for external suggestions under suggestions/ (this is the Inbox), recorded agent handoffs under handoffs/, an agent report-back drop-zone under reports/, and a regenerated human-readable ROADMAP.md mirror. Shared cards that many projects reference live under a top-level shared/items/, and archived projects move to archive/ while staying fully readable.

Structuring it this way buys four properties that genuinely matter. Every artifact is inspectable — a file you can cat, diff, grep, and commit. It’s portable with no lock-in — back it up with cp, sync it with Dropbox or Syncthing or git, migrate it trivially. It’s interoperable — any tool in any language can join just by reading and writing files. And it’s restartable — there’s no in-memory state to lose, because the process is stateless over the files. None of those is a marketing bullet; each is a direct consequence of refusing to put a database in the middle.

Making file-based state actually safe

“Just use files” is easy to say and easy to get wrong. Two disciplined patterns, ported deliberately from a sibling app’s battle-tested code, are what make it safe.

The first is atomic writes. Every write goes to a temp file in the same directory, then rename()s over the target. Because rename is atomic on a single filesystem, a crash mid-write can never truncate the source of truth — you’re left with either the complete old file or the complete new one, never a half-written corruption. It’s a handful of lines, and it’s the difference between “files as a database” being reckless and being sound:

async function writeFileAtomic(path: string, contents: string) {
  await mkdir(dirname(path), { recursive: true });
  const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
  await writeFile(tmp, contents, "utf8");
  await rename(tmp, path);
}

The second is read-merge-write with tolerant normalization. Updates read the current file, spread-merge the patch, preserve id and createdAt, bump updatedAt, and write atomically. Reads are deliberately tolerant: missing fields get sane defaults, invalid scores are clamped to range, and — crucially — unknown keys are preserved. That last detail is what makes the contract forward-compatible. A newer tool can write extra fields, and an older Threlmark will round-trip them untouched rather than dropping them. The contract can grow without breaking the tools that already speak it.

One file per item, and a board that heals itself

There’s a tempting naive design where a project’s roadmap is one roadmap.json array. It’s also a concurrency hazard: every writer has to read-merge-write the whole list, so two tools writing at once race and clobber each other. Threlmark instead uses one file per item (items/.json). Single-file atomic writes are collision-free, so an external tool can drop or update a card without coordinating with anyone — no locks, no negotiation, no shared mutable list.

Lane ordering lives separately, in board.json, as ordered arrays of item IDs per lane. The clever part is that the board self-heals on read: it’s reconciled against the actual item set every time it’s read. Any item present in items/ but missing from its status lane gets appended; any ID whose file is gone gets dropped. The consequence is quietly powerful — an external tool never has to touch board.json at all. It writes an item file; the board fixes itself the next time Threlmark reads. The hardest part of letting outside tools participate, keeping the ordering structure consistent, simply evaporates.

IDs follow a sortable, collision-resistant convention. A projectId is a slug of the name — stable and human-readable. An itemId or suggestion ID is --, so it sorts chronologically and won’t collide. Imported cards keep their original ID, which makes re-importing the same source idempotent: it updates rather than duplicates. Cross-project references use a global address — "/", or "shared/" for shared cards — so any card anywhere has exactly one canonical name.

The data model and the score that’s never stored

A roadmap item carries the original Roadmap-Lab card shape plus flow fields: a title and category, a status (idea / ranked / development / done), the four 1–5 axes (impact, evidence, fit, effort), a description, target files, acceptance criteria, and then the flow-specific additions — an append-only transitions[] lane history, an optional handoff stamp, and a reports[] array. And it carries [extra: string]: unknown — the explicit preservation of unknown keys that makes the whole contract extensible.

The detail engineers will appreciate most: priority is never stored. It’s computed on read, so it can never drift from the axes that define it:

priority = max(0, round(impact·3 + evidence·2 + fit·2 − effort·1.5))

Impact is weighted heaviest; effort is the only axis that subtracts. The formula is reused verbatim from the original tool, so imported cards rank identically — a 5/5/5/4 card scores 29 there and 29 here. Deriving rather than storing is a theme: anything that can be computed from item state is, so the displayed numbers can never disagree with the underlying files.

Schema versioning is handled the same unglamorous way. The current schemaVersion is 2; the bump from 1 added transitions. Migration is lazy and per-file — a v1 item with no history is migrated on read by seeding a single transition from its createdAt and current status. No big-bang migration step, no downtime, and old files (and external tools that ignore the new fields) keep working.

Flow as a layer on top of status

The original board answered “what column?” — that’s status tracking. The modern Kanban Method manages flow: limit work-in-progress, measure how work moves, improve incrementally. Threlmark adds that as a layer on top of the board without throwing the board away, and the enabling change is that append-only transitions[] history. From it, four things derive directly. Work-item age is now minus when the card entered its current lane; past a per-lane threshold (development 7 days, ranked 21, idea 60) it’s flagged stale with a red badge. Cycle time is the span from first entering Development to reaching Done. Throughput is items reaching Done per ISO week over an eight-week window. And WIP is the current count per lane, flagged when it exceeds the configured cap — the header shows 3 / 2 in red.

These are computed by a pure metrics module with no stored aggregates, so the numbers can never contradict the board. A per-project Flow tab renders throughput, cycle time, aging work, and agent throughput; the home page shows a portfolio-wide flow strip. Equally important is what’s deliberately out of scope: no SLAs, no SAFe or enterprise portfolio machinery, no deterministic delivery-date forecasting. Threlmark is a solo and small-team builder’s tool, and importing the enterprise apparatus would trade one kind of wrong-fit for a heavier one. Knowing what to leave out is part of the architecture.

The closed agent loop

This is the part that’s genuinely 2026-shaped. More and more of the building is done by AI coding agents, so Threlmark treats a handoff as a first-class flow event and closes the loop so you don’t babysit it.

From a card, you generate an implementation brief — a file-scoped Markdown prompt carrying the description, the target files, the acceptance criteria as checkboxes, and verification commands (npm run typecheck && lint && build). “Generate & mark handed off” records the handoff under handoffs/, stamps the item with handoff:{handoffId, agent, at}, and optionally moves it to Development. The brief also includes a reporting protocol: it tells the agent to report started / done / blocked / failed, with a summary and the commands it ran, and it carries the exact ready-to-run commands — the API base URL auto-detected from the request origin, the per-item IDs embedded.

The agent reports through either of two channels, and the redundancy is the point. The preferred path is REST: POST /api/projects/:id/items/:itemId/report. The fallback is the filesystem: drop a JSON file into the project’s reports/ folder, and Threlmark ingests it on read — applying the report, then moving the file to reports/.applied. That makes the loop robust even if the server isn’t running at the exact moment the agent finishes. A done report auto-moves the card to Done; the board polls /reports, surfaces a live toast (🤖 claude done: …), and the card moves on its own. The Flow view then counts brief-to-shipped time, agent throughput by agent, and stalled briefs — handed off but not Done after seven days. The full loop reads cleanly:

rank → hand off (brief tells the agent how to report) →
agent builds AND reports → card lands in Done → Flow counts brief → shipped

Cross-project operations and the portfolio score

Because items are globally addressable, four cross-project moves fall out naturally. You can move an item to another project — the file is rewritten under the new project, removed from the old, both boards self-heal, and any link addresses repoint. You can link or depend — an edge in links.json typed blocks / relates / duplicates, where blockers earn a small bottleneck boost in portfolio ranking and a visual flag. You can share — one canonical card in shared/items/, with each consuming project holding a thin sharedRef pointer and provenance recorded as duplicates links. And you can promote a suggestion cross-project, accepting an incoming suggestion into a different project via targetProjectId.

The Portfolio ranks every active project’s items together by a status-weighted score, so in-flight work floats to the top:

score = priority · statusWeight  (+ 0.1 · blockedCount · priority)
statusWeight = { development: 1.3, ranked: 1.0, idea: 0.85, done: 0.15 }

Finishing beats starting (development outweighs ranked), and blockers get nudged up because bottlenecks are the most expensive thing in the system. It’s a small formula doing a lot of the product’s most useful work.

The REST API and the importer

The UI’s server components and external tools call the same store functions — the REST API is a thin layer over the identical code path, not a parallel implementation that can drift. It exposes the expected surface: list/create projects, get/update/archive a project, list (with computed priority) and create items, move a card within or across projects, read/patch the board order, the suggestions Inbox with accept and dismiss, handoff generation, agent report-back and the reports feed, project and portfolio flow metrics, and the cross-project links, shared, and portfolio endpoints. The conventions are boring in the good way: tolerant body parse or 400, required-field validation or 400, create returns 201, mutations return the persisted entity.

There’s also an importer worth a mention, because it’s the bridge from the old world. The original roadmap lived in an HTML file as a JavaScript const defaults = [ ... ] array — object literals with unquoted keys, not JSON. The importer extracts that array with a string-aware bracket scan and parses it with JSON5 (never eval). Each card becomes an item keeping its original ID, so re-import is idempotent, and the board reconciles by status afterward.

The contract in action — and the honest deployment caveat

The disk contract isn’t theoretical. A sibling app, IdeaClyst — an idea engine that turns product concepts into founder plans and scouts the web for opportunities — plugs straight in by adding a layer that speaks Threlmark’s exact shapes. It reads roadmaps read-only (listing projects, reading items and board.json, computing the same priority, building a deterministic gap map of category coverage and lane counts), and it writes only suggestions — dropping suggestions/.json via the same atomic pattern, in the flat shape Threlmark expects, with its provenance carried in those preserved extra keys. A ThrelmarkSource abstraction lets it write straight to disk or via REST, with REST falling back to the disk writer if the server is unreachable, so a suggestion is never lost. That’s the whole thesis demonstrated: any tool that speaks the contract can join the same loop by reading roadmaps and dropping files. The fourth article in this series covers that integration end to end.

One honest closing note on hosting, because local-first shapes deployment. Threlmark needs a Node.js runtime — its API reads and writes files at request time — so ordinary static or PHP shared hosting can’t run it. There are three honest paths: a static read-only demo (seeded data, writes to localStorage); a password-gated personal instance on a small Node host with a persistent, backed-up THRELMARK_DATA_DIR; or a true multi-tenant SaaS, which would mean adding accounts and per-tenant data isolation and is a separate build. The store interface (src/lib/*/store.ts) is the natural seam for swapping disk for per-tenant storage — which is the elegant part. The same boundary that makes the local tool simple is the one you’d extend to make it multi-tenant. The architecture doesn’t fight that future; it just doesn’t pay for it until you need it.

That’s the system: a small, inspectable codebase where the hardest property — safe concurrent file access as a shared contract — is handled by two disciplined patterns instead of a database. Disk is the contract, the board heals itself, priority is derived not stored, and the agent loop closes on its own. Not much machinery, and exactly enough.


Threlmark is open source under the MIT license. Source: github.com/MeyerThorsten/threlmark · Site: threlmark.com.

© 2026 Threlmark · Thorsten Meyer · Powered by Thorsten Meyer AI

You May Also Like

Generative AI in Enterprise: Case Studies of Real Business Applications

Incorporating generative AI into enterprise operations reveals compelling case studies that showcase innovative business transformations and competitive advantages.

SEO Is Not Dying — It’s Being Absorbed by AI

By Thorsten Meyer AI For years, SEO has been treated as a…

Meta-Harness: The Code Around the Model Matters More Than the Model

Thorsten Meyer | ThorstenMeyerAI.com | March 2026 Executive Summary The performance gap…

From Conversation to Commerce: How Google’s AP2 and Coinbase’s x402 Could Unlock an Agentic Payments Economy

Published context: Google announced the Agent Payments Protocol (AP2) on September 16,…