This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Workspace-level Go rules live in /Users/vampire/go/src/AGENTS.md (library choices, code style, lint thresholds, release workflow). This file only covers what is specific to afk.
afk is a local SQLite-backed task queue for coding agents and shell workflows. A task has a lifecycle (todo → doing → done/failed/deleted), optional dependencies (--blocked-by), resource locks, and leases. Process supervision is external: workers use afk take, execute the task, then afk set.
afk prompt emits Markdown that Claude Code's /loop re-runs on an interval — that is the intended pattern for driving an agent off the queue. See README.md.
Module path: github.com/dotcommander/afk. Go 1.26.
go build -o afk ./cmd/afk
mkdir -p ~/go/bin
rm -f ~/go/bin/afk
install -m 0755 afk ~/go/bin/afkVerification (per workspace rules — pipe long output through tail):
go build ./...
go test ./... | tail -50
go vet ./...
golangci-lint run ./... | tail -50Run a focused package test when needed, for example go test ./internal/commands -run TestPrompt -count=1.
No Makefile. The committed afk binary at the repo root is a build artifact, not a script.
Registered in internal/commands/root.go (func NewRoot). Categories:
- Inspection:
tasks,task,status,find,snapshot - Scheduling:
add --blocked-by,relate,gate,take --dry-run - Lifecycle:
add,set - Goal:
goal <objective>(compile→approve→insert as a dependency chain),goal status <goalID>,goal audit <taskID> - Worker:
take, hiddenheartbeat, hiddenrequeue-stale - Meta:
prompt - Web:
serve
Global persistent flag --queue <path> (also AFK_QUEUE env var). Resolution order: flag → env → ~/.claude/queue/tasks.sqlite.
cmd/afk/ main() → run(ctx); signal.NotifyContext; builds Deps and calls commands.NewRoot
internal/app/ Service (use cases) over the Store interface; ExplainData
service_helpers.go unexported leaf helpers
internal/commands/ Thin cobra wrappers; Deps{} passed by pointer
add_options.go input/option-building helpers for 'afk add'
internal/output/ Human and JSON/JSONL rendering
internal/prompt/ Generates Claude Code /loop instruction Markdown
internal/store/ SQLite persistence; Paths, ResolvePaths, NewSQLite; schema DDL inline
sqlite_schema.go schema DDL, migration, busy-retry helpers
sqlite_scan.go row scan/encode helpers
internal/server/ HTTP dashboard server; routes, handlers, and go:embed'd web/index.html SPA
internal/task/ Domain types (Task, Status, Event, Attempt, Dependency, Block); no I/O
The Store interface is defined in internal/app/store.go (package app, not package store), and internal/store/sqlite.go provides the concrete SQLiteStore implementation, verified by a compile-time var _ Store = (*store.SQLiteStore)(nil) assertion in internal/app/store.go.
modernc.org/sqlite (pure Go — no CGO). Base tables are created idempotently at NewSQLite() open: tasks, metadata, task_events, task_attempts, task_dependencies, task_gates, request_ledger, task_checkpoints, task_artifacts, vybe_imports, goal_groups, goal_iterations, plus the tasks_fts FTS5 virtual table with sync triggers. Schema DDL lives inline in internal/store/sqlite_schema.go. Additive changes for existing DBs are applied by a lightweight in-process versioned migration runner (runMigrationsIfNeeded dispatching e.g. migrateV7GoalOutcome, migrateV8FTSUpdateScope, backfillTasksFTS) — not goose or any external framework; migrations are idempotent (duplicate-column errors absorbed, triggers dropped and recreated).
SQLite is the only queue backend. A --queue/AFK_QUEUE path with a non-.sqlite extension (including a stale .jsonl path) is normalized to a sibling .sqlite database; it is never read as JSONL.
afk promptdoes not open the DB unless--task <id>is given. Controlled by theskipStoreInitcobra annotation helper ininternal/commands/root.go(func skipStoreInit). Don't accidentally remove that — it's what lets/loopcallafk promptcheaply.afk runis not public. Useafk take --dry-runfor readiness previews and external loops for execution.- Worker contract: preview with
afk take --dry-run --json --fullwhen triaging; claim withafk take --worker <name>, then explicitly finish with the same identity viaafk set <id> done --note <evidence> --worker <name>orafk set <id> failed --note <reason> --worker <name>. Use--summarywhen a receipt should include queue counts. A terminal transition (done/failed) requires a note as completion evidence — an empty note is rejected withErrMissingCompletionNote. An unqualified terminal set cannot close a named worker's active attempt;--forcewithout--workeris the explicit administrative override. - Targeted retry: inspect the task, then use
afk retry <id> --reason "<reason>"orafk set <id> doing --note "retrying: <reason>"to open a fresh attempt before doing retry work. - Readiness has one authority:
store.Ready(SQL) is the single source of truth for whether a task is ready.takeandtake --dry-runconsult it directly. Change the readiness predicate only instore.Ready. - No viper. Queue path comes from flag/env/default — there is no config file layer. Don't add one without a reason.
afk goalis fail-closed and durable. It reads~/.config/afk/goal.yaml(yaml.v3, written with defaults on first run — not viper) wheresetup_command/audit_commandare empty by default;afk goalerrors withErrGoalSetupNotConfigureduntil a setup agent command is configured (file or--setup-command), andgoal auditerrors until an audit command is set. The objective is HTML-escaped before prompt interpolation. Nonzero token caps require a one-capturetoken_regex; missing/overflowing usage suspends remaining work astoken-usage-unavailable. Budget accounting is SQLite-owned and idempotent by attempt;goal resumepreserves cumulative usage and resets the duration epoch.afk servebinds127.0.0.1by default (loopback only — task bodies may be sensitive); supplying a non-loopback--addrprints a warning to stderr. The front-end is a singlego:embed'dweb/index.html(no build step required); opens a browser tab by default (--open=falseto suppress).
cmd/afk/main.gostays small (~37 lines): delegates torun(ctx context.Context) error, letsdeferfire beforeos.Exit. Don't grow it.internal/commands/*are thin cobra wrappers; business logic belongs ininternal/app/.- Domain types in
internal/task/have no I/O — keep it that way. - Linting matches workspace defaults (
.golangci.ymlv2,default: nonewith explicit enables). Test files are exempt fromdupl,goconst,funlen,gocognit,gosec.