Skip to content
PortBay

Git-native task tracking: every card is a Markdown file

No database, no export button, no API to reverse-engineer. A card is .portbay/tasks/<id>.md — YAML frontmatter and a Markdown body — and the file is the source of truth, not a cache of one.

The short answer

Can I keep my task board in my git repo as Markdown files?

Yes, and a few tools now work this way: one task per Markdown file with YAML frontmatter, kept beside the code instead of stored in a hosted database. PortBay's board is built on that. Every card is one file at .portbay/tasks/<id>.md — frontmatter for title, status, priority, labels, estimate, due date and the verify gates, and a Markdown body that is the description. Change status: Todo to status: InProgress in any editor, save, and the board moves the card about 150 milliseconds after the writes stop. Agents get two lanes: an audited one over MCP or the CLI, which takes a per-card lock and writes an attributed audit entry, and a raw lane that edits the file directly. In a git repository the canonical copy is a git ref, refs/portbay/board, so the board no longer changes with the branch you have checked out.

Verified against the product, 2026-09-06.

In the app

The same card, twice: a board column and a text file

Board ⟷ .portbay/tasks/t_01HF9K7Q….md
Boardportbay
To Do1
Fix the login bug
p0authest 3
In Progress0
File.portbay/tasks/t_01HF9K7Q….md
---
id: t_01HF9K7Q2M3B4N5P6R7S8T9V
title: Fix the login bug
status: Todo
priority: high
labels: [auth, p0]
verifyChecks: npm test -- auth
---

One line changes in .portbay/tasks/t_01HF9K.md — status: Todo becomes status: InProgress — and the card crosses the board about 150 ms after the write lands.

How it works

From a text editor to the board and back

  1. Read the contract that ships beside the cards
  2. Write a card as a file
  3. Move it by editing one line
  4. Or take the audited lane
  5. Write safely when two things write at once

Read the contract that ships beside the cards

PortBay writes .portbay/tasks/AGENTS.md into the same directory: the card anatomy, every frontmatter field and who is allowed to edit it, the two edit lanes, and the locking discipline. It is generated, safe to read, and the one file in that folder that is not a card.

cat .portbay/tasks/AGENTS.md

Write a card as a file

Frontmatter plus a body. The id is a stable ULID you never change; status is the column; title, priority, labels, estimate, acceptance, verifyChecks and doneWhen are yours to edit. Any key PortBay does not recognise is round-tripped under the card's custom map instead of being dropped.

.portbay/tasks/t_01HF9K7Q2M3B4N5P6R7S8T9V.md

Move it by editing one line

Set status to another column and save. The watcher coalesces a burst of writes, waits for about 150 milliseconds of quiet, reconciles the external edit and re-renders once — so a script that rewrites forty cards produces one board update, not forty.

sed -i '' 's/^status: Todo$/status: InProgress/' t_01HF9K.md

Or take the audited lane

The raw lane is attributed to “edited on disk (System)”, because a file write carries no identity. Going through MCP or the CLI instead takes the per-card lock, honours your run id, and puts your name on the entry in the activity feed. Comments only exist on that lane — a file cannot carry an audit thread.

portbay tasks edit <project> <card> --title 'Fix the login bug'

Write safely when two things write at once

For a read-modify-write, take the advisory per-card lock; for a whole-file replacement, write a temp file and rename over the card. Either way a concurrent PortBay write cannot clobber yours, and yours cannot clobber PortBay's.

.portbay/.runtime/cards/<id>.lock
Spec

What the file format guarantees

CapabilityCommunityPro
One Markdown file per cardIncludedIncluded

.portbay/tasks/<id>.md — YAML frontmatter and a Markdown body. The file is the durable source of truth, not a cache.

Board re-render after a file edit~150 ms~150 ms

The watcher drains a burst of writes and re-renders once the writes stop, so a bulk rewrite costs one update.

Frontmatter fields you may editDocumentedDocumented

title, status, priority, labels, estimate, acceptance, verifyChecks, doneWhen, spec, touchpoints, due — each one documented with its owner.

Human-only fields22

signedOff and blockedExitApproval. An agent must never write either; a sign-off dated before the card's current claim is refused.

Audited edit laneIncludedIncluded

portbay_task_edit over MCP, or portbay tasks edit. Takes the per-card lock and writes an attributed audit entry.

Raw file laneIncludedIncluded

Edit the .md with any tool. PortBay records an “edited on disk” System entry but cannot attribute the change to you.

Per-card advisory lockIncludedIncluded

.portbay/.runtime/cards/<id>.lock, or write atomically with a temp file and a rename.

Canonical store in a git repositorygit refgit ref

refs/portbay/board — a ref git never checks out, shared by every worktree of the repo. Updates are compare-and-swap; conflicting edits are three-way merged, never clobbered.

Unrecognised frontmatter keysPreservedPreserved

Round-tripped under the card's custom map rather than dropped or rejected, so your own fields survive a PortBay write.

Verified against the app source and the generated .portbay/tasks/AGENTS.md. The board ships in every tier; there is no board entitlement gate.

Without PortBay

The two alternatives, and what each one costs

Task tracking usually lands in one of two places: a single TODO.md that every change collides in, or a hosted board whose only exit is an export endpoint.

One file per card gives every task its own history, its own diff, and a path an agent can read with cat.

Questions

Asked before downloading

Not to your branch, by default — and the reason is a bug that fix closed. Cards used to be ordinary tracked files, so the board showed different counts depending on which branch was checked out, and an uncommitted card closure was invisible to everyone including the app. PortBay now keeps the canonical board in refs/portbay/board, a git ref that is never checked out and that every worktree of the repository shares, and writes a .gitignore in .portbay/tasks/ so the working copies stop being branch-scoped. The cards are still plain Markdown on disk and still versioned by git — in a ref rather than in your branch. Set PORTBAY_BOARD_REF=0, or delete the ref with git update-ref -d refs/portbay/board, to go back to ordinary tracked files; nothing was moved, so nothing is lost either way.

Yes. Write .portbay/tasks/<id>.md with an id, a title and a status and the board picks it up on the next watcher pass. The one discipline that matters is atomicity: write a temp file and rename it over the card, or take the advisory lock at .portbay/.runtime/cards/<id>.lock for a read-modify-write, so a concurrent PortBay write cannot land halfway through yours.

Because .portbay/tasks/.gitignore ignores *.md, and ripgrep — plus any grep wrapper that respects gitignore — skips ignored files by default. A recursive search returns zero matches for strings that are definitely there, silently. Use an explicit glob, rg --no-ignore, or find .portbay/tasks -name '*.md' -print0 | xargs -0 grep -l. This is a real footgun and it is written down in PortBay's own rule-book for exactly that reason.

The file format is the same idea, and that is deliberate. What is different is what sits on top: a board a dispatched agent can move a card on over MCP with an audit trail, verify gates and doneWhen signals that decide when a card is allowed to reach Done, a per-card lock so concurrent writers do not clobber each other, and a canonical store that is a git ref rather than the checked-out branch. If you only need a text file with checkboxes, a TODO.md is genuinely fine.

Every board-ref update is a compare-and-swap, so a write cannot silently overwrite another process's. A card that moved on both sides is merged with git merge-file over ours, the last-synced base and theirs, so two edits to different parts of one card both land. If the same lines moved on both sides, nothing is dropped: your local file is left alone, the other version stays at the ref tip, and a full copy of it is parked at .portbay/tasks/.conflicts/<id>.<tag>.md until a person picks one.

PortBay mascot — a friendly blue tugboat

Give your projects and your agents a real local home.

Download for macOS

Free & open source · macOS 11+ on Apple Silicon · Pro from $10/mo