Metadata-Version: 2.4
Name: taskkit-md
Version: 0.1.0
Summary: A plain-Markdown task queue for local AI workers - no database, no server, stdlib only.
Author: Trashpanda62
License: MIT
Project-URL: Homepage, https://github.com/Trashpanda62/taskkit
Project-URL: Repository, https://github.com/Trashpanda62/taskkit
Project-URL: Issues, https://github.com/Trashpanda62/taskkit/issues
Keywords: tasks,queue,markdown,llm,agents,local-first
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# taskkit

> 📖 **Looking for the guide?** → **[Actually Using LLMs Properly](GUIDE.md)** — context windows, real token costs, local models, and giving your AI a memory. This package is Part 9 of it.

A task queue for local AI workers where **the queue is a folder of Markdown files**.

No database. No server. No cloud. No dependencies outside the Python standard
library. You can edit the queue in Obsidian, VS Code, or Notepad while the
workers are running, and `git diff` shows you exactly what a robot did.

```
- [ ] Summarise the Q3 support tickets | owner: writer | priority: P1
- [/] Draft the release notes          | owner: writer | done: 2026-07-25
- [x] Rename the config keys           | owner: coder  | done: 2026-07-24
- [ ] Sign the lease                   | cat: human
```

That's the whole data model. Everything else in this package is machinery for
running those lines safely.

---

## Starter vault included

`starter-vault/` is a ready-to-open Obsidian vault with the memory convention
from the guide pre-wired: a `memory/` index your AI loads each session, four
worked example memories, note/task/plan templates, a taskkit queue folder,
and a zero-dependency `search.py`. Open it as a vault, replace the examples,
and your assistant has a persistent brain made of plain text. Details in
[starter-vault/README.md](starter-vault/README.md).

## Why this exists

Most "AI agent" setups store state somewhere you can't see — a vector DB, a
SaaS dashboard, a JSON blob with no history. When the agent does something
stupid at 3am you have no idea what it thought it was doing.

Plain Markdown fixes that. It's greppable, diffable, version-controlled by
whatever you already use, readable by every model ever trained, and editable by
a human who has never heard of your tool.

## Install

```bash
pip install taskkit-md
```

Python 3.10+, Windows and Linux, zero runtime dependencies. (The dist is
`taskkit-md` — plain `taskkit` was taken — but the import and the command are
both still `taskkit`.) Prefer no install at all? `git clone` and use
`python -m taskkit` everywhere you see `taskkit` below.

## The 60-second path

```bash
taskkit init                 # scaffolds tasks/, output/, taskkit.json
echo "- [ ] Explain what taskkit does in one paragraph" >> tasks/writer.md
taskkit run --once           # a worker claims it, runs it, files the output
taskkit status               # "1 item awaiting your review"
```

Open `output/writer/` to read what it produced, then `taskkit accept writer
<id>`. That's the whole loop: checkbox in, reviewed deliverable out. Wire a
real engine (Ollama, Claude Code) in `taskkit.json` — see
`examples/claude-code/` for the 4-line Claude wiring.

## Five-minute tour

```bash
export ROOT=~/mytasks

python -m taskkit --root $ROOT init
python -m taskkit --root $ROOT add writer "Explain what this repo does in 150 words" --field priority:P1
python -m taskkit --root $ROOT add writer "Sign the contract" --field cat:human

python -m taskkit --root $ROOT list
python -m taskkit --root $ROOT run --once      # workers drain the queue
python -m taskkit --root $ROOT status          # "1 item awaiting your review"

cat $ROOT/output/writer/*.md                   # read what it produced
python -m taskkit --root $ROOT accept writer t-abc1234567
```

Note what did *not* happen: the `cat: human` task was never touched, and
nothing reached `[x]` without you typing `accept`.

## Wire it to a local model

`taskkit.json` maps each queue to a runner:

```json
{
  "owners": {
    "writer": {
      "runner": "ollama",
      "model": "qwen3:8b",
      "host": "http://127.0.0.1:11434",
      "num_ctx": 8192,
      "system": "You are a careful writer. Answer directly. No preamble."
    },
    "coder": {
      "runner": "shell",
      "command": "python tools/handle_code.py {text}",
      "timeout": 900
    },
    "human": { "runner": "echo" }
  }
}
```

Then leave it running:

```bash
python -m taskkit --root $ROOT run --interval 300 --min-interval 30
```

A cycle that got work sleeps 30s so a backlog clears fast; an idle cycle relaxes
to 300s so an empty queue costs nothing.

### Custom runners

A runner is any callable `(task, cfg) -> str`. Drop this anywhere that gets
imported:

```python
from taskkit.runners import register

@register("my-api")
def run_my_api(task, cfg):
    resp = call_whatever(task["text"], model=cfg["model"])
    return resp.text          # return the output, or raise to fail the task
```

Runners never touch the queue. They take a task and return text; claiming,
completing and the review gate are the store's job. That separation is what
lets you swap a local 8B model for an API call for a bash script without
changing anything else.

---

## The three rules that make it work

### 1. A worker cannot accept its own work

There are three checkbox states, not two:

| box   | meaning |
|-------|---------|
| `[ ]` | open |
| `[/]` | done-pending-review — a worker produced output, nobody has accepted it |
| `[x]` | accepted |

A worker can only ever move `[ ] → [/]`. The only paths to `[x]` are a human
running `accept`, or external proof that the outcome actually happened.

This sounds like bureaucracy until you skip it. An agent that can mark its own
homework will report a 100% success rate while producing nothing, and you will
believe it, because the dashboard is green. Make the machine *unable* to lie
about completion rather than trusting it not to.

Watch for the softer version of the same failure: a "review" step that closes
items on a timer. If unreviewed work auto-closes after N days, `[x]` means
*expired*, not *accepted*, and your metrics are fiction again.

### 2. Claim before you work

Every worker writes a lease onto the task line before running it:

```
- [ ] Summarise the tickets | claim: rig01-48213-1769342115
```

`host-pid-epoch`. Another worker sees a live claim and skips the task. If a
worker crashes, the claim goes stale after an hour and the task is reaped back
into the pool automatically.

Two things here are correctness requirements, not polish:

- **Sanitise the hostname.** The freshness check reads the epoch by position.
  A machine named `DESKTOP-A1B2C3` breaks the parse, every claim reads as
  unparseable, every worker thinks every task is free, and mutual exclusion
  silently evaporates — on some machines and not others.
- **Give each in-process thread a distinct worker suffix.** Two threads
  claiming in the same second mint identical stamps, and since completion
  matches on the exact stamp, one thread can finalise the other's task.

### 3. Every field must survive the round trip

`parse_line` → `serialize_task` has to be lossless. A field that gets dropped
doesn't throw — it just quietly stops working. Drop `claim` and two workers run
the same task. Drop `recur` and a recurring task never comes back. Adding a
field means adding it to `FIELDS` in `tasks.py`, and there's a test that fails
if you forget.

Related: **don't split the task line on a bare `|`.** Any task whose text
contains a pipe — a shell command, a table row, an `a|b` key — gets cut in half
with no error. taskkit peels known fields off the right-hand side instead, so
``Run `grep foo | wc -l` and report`` round-trips intact. There's a test for
that too, because this bug is invisible until it's expensive.

---

## Gating: things a robot must never pick up

Any of these on an open task makes it invisible to automation:

| field | meaning |
|-------|---------|
| `cat: human` / `physical` / `credential` | needs hands, a body, or a password |
| `review: 1` | first run of a new automation — a human eyeballs it once |
| `approve: pending` | blocked on a decision |

A human can still claim a gated task deliberately. Automation cannot, ever.

The failure this prevents is specific and real: an autonomous conductor once
claimed *"fix the car"* and sat on it for eleven days, because to a task queue
it looked exactly like every other line.

---

## Design notes

**Failures reopen the task.** A failed run clears the claim, records the error,
and puts the task back to `[ ]`. It'll be retried next cycle — but *not* in the
same cycle, or a permanently broken task becomes a hot loop that hammers your
model server all night and never gets to anything else.

**Empty output is a failure.** A runner that returns nothing has failed, even
if it exited 0. Half a document is worse than no document, because a human
skims it and assumes it's finished.

**Output goes on disk, one file per task**, under `output/<owner>/<id>.md`. The
file — not the model's own word — is what a reviewer looks at. Without an
artifact, "review" means clicking accept on a claim you can't check.

**Locking is a lockfile, not `fcntl`/`msvcrt`.** Identical behaviour on both
platforms, and a human can `ls` to see who holds it. Stale locks are cleared
after 2 minutes.

**Writes are atomic** (temp file + `os.replace`), so a crash mid-write can never
truncate your queue.

**Force UTF-8 on stdout in anything that prints model output.** Under Windows
Task Scheduler, Python defaults stdout to cp1252 and *raises* on the bullets,
arrows and curly quotes that LLM output is full of. The traceback lands after
the model already did the work — full cost, zero result — and it only reproduces
headless, so testing by hand always passes. At one point this was silently
killing 34% of dispatches on the box that taught me this.

**Exit code 0 is not success.** A tool that printed `"<engine> unavailable:
..."` and exited 0 meant every caller filed an outage as a completed
deliverable. Define success explicitly — never trust the exit code alone.

**A truncated generation must fail, not become a draft.** When the model
reports it stopped on a length limit, that's a failure. Half a document is
worse than none, because a human skims it and assumes it's finished.

**`Path.exists()` is not verification.** It happily marks an empty or
malformed file as done, and it misreads negated criteria entirely — "no
console errors" passes vacuously on a file that says nothing at all. Only
check something you can actually evaluate.

**Shadow mode is how you ship an autonomous loop people will tolerate.** Run
the decision logic for N cycles, log what it *would* have done, then go
live. Keep the decide step a pure function of a snapshot — no clock, no
disk, no network — so shadow mode is a faithful preview, not an
approximation.

**Adaptive cadence needs asymmetry.** A productive cycle sleeps the floor;
an idle cycle relaxes toward the ceiling; failures never shorten the nap.
Never hammer a host that's down.

---

## Layout

```
taskkit/
  tasks.py      the line format: parse, serialize, gating rules
  store.py      the queue files: locking, claim/complete/release, reaping
  runners.py    pluggable executors (echo, shell, ollama) + @register
  worker.py     the drain loop and adaptive cadence
  cli.py        the command line
tests/
  test_taskkit.py    core engine tests
  test_packaging.py  CLI/packaging surface (init, examples, entry point)
  test_starter_vault.py  starter-vault search.py
  44 tests total, no network, no model required
```

```bash
python -m pytest tests -q
```

## Scheduling it

**Linux** — `crontab -e`:
```
*/5 * * * * cd /home/you/mytasks && /usr/bin/python3 -m taskkit run --once >> cron.log 2>&1
```

**Windows** — use `pythonw.exe` as the action, never `cmd` or `python`, or a
console window flashes on screen every five minutes forever.

## Scope

taskkit is the boring, reliable half of an agent system: durable queue, safe
concurrency, honest completion. It deliberately does **not** do planning,
retrieval, or multi-agent orchestration. Those change every six months. A task
queue you can read in a text editor does not.

## Licence

MIT.
