Durable continuity for AI coding agents. Vybe gives autonomous agents crash-safe task tracking, append-only event logs, scoped memory, deterministic resume/brief, and artifact linking — all backed by SQLite. Agents pick up exactly where they left off across sessions without human intervention.
This is a global CLI tool installed system-wide.
| Path | Purpose |
|---|---|
~/.config/vybe/config.yaml |
User settings |
~/.config/vybe/vybe.db |
Runtime state (SQLite) |
go build -o vybe ./cmd/vybe
#-Option 1: Standard install
go install ./cmd/vybe
#-Option 2: Symlink (keeps binary in project, linked to ~/go/bin)
ln -sf "$(pwd)/vybe" ~/go/bin/vybeConfig is loaded from (in order, first found wins):
~/.config/vybe/config.yaml/etc/vybe/config.yaml./config.yaml(current directory; lowest priority)- Environment variables (prefix:
VYBE_)
Relevant keys:
db_path(in config.yaml)VYBE_DB_PATH(env override)--db-path(CLI override; highest priority)default_agent(in config.yaml) — persistent agent identity fallback; resolution order:--agentflag →VYBE_AGENTenv →config.yaml: default_agent
State is persisted in SQLite and managed through the CLI commands (tasks, events, memory, agent state).
Agents-Only CLI - Continuity primitives for autonomous agents.
Beta — No backward compatibility, migration support, or deprecation shims. Breaking changes are acceptable. Rename flags, remove commands, change schemas freely.
Humans may read logs for debugging, but the product is not designed around human interaction.
- Agent memory system (memories, compaction, GC, reinforcement)
- Multi-agent coordination (agent state, focus tasks, claims, heartbeats, dependencies)
- Claude Code hook integration (6 hook events, bidirectional context injection)
- Event stream (structured log of agent activity — prompts, tool calls, spawns, completions)
- Idempotent operations (request ID deduplication across all mutations)
- Session continuity (resume with context, auto-summarization)
- Not a human issue tracker (no tags, epics, comments, kanban, search-by-keyword)
- Not a project management tool (no dashboards, no reporting)
- Not a general-purpose CLI tool (hooks are hidden, output is JSON for machine consumption)
- Agents know their task IDs — they don't need search
- Agents emit structured metadata — they don't need tags
- Events ARE the comment stream — no separate comment entity needed
- Projects are the only grouping level — no epics hierarchy
- Dependencies (blocks/blockedBy) model all task relationships — no subtask entity needed
- Every mutation is idempotent — agents retry freely without side effects
docs/is for vybe users only (operators and integrators using the tool).- Do not place work-in-progress notes, temporary writeups, local-dev scratch files, refactor journals, or historical snapshots in
docs/. - Historical/audit/scratch material must stay outside tracked docs (for example in
.work/), and must not be staged for commit. - Contributor implementation process belongs in
CLAUDE.mdand code comments/tests where needed, not user docs.
- Use
.work/for local non-user artifacts. - Preferred local structure:
.work/scratch/for in-progress notes.work/audits/for audit outputs.work/refactors/for refactor/implementation journals.work/archive/for local historical snapshots
- Use
/tmp/vybe-*only for ephemeral one-run artifacts that can be safely lost. - Never stage
.work/**or/tmpartifacts for commit.
- No human-in-the-loop requirements.
- Do not introduce workflows that require a human to approve, confirm, click, or provide input in order to make progress.
- Avoid statuses like
needs_user_inputor "blocked on user". If something is blocked, it must be blocked on an external system/time, and the system must be able to retry/backoff autonomously.
- Non-interactive by default.
- No prompts, no TTY UIs, no "Are you sure?" confirmations.
- If an operation is dangerous, require an explicit flag (e.g.
--force) and fail closed without it.
- Machine-first I/O.
- All commands that are part of the agent workflow must emit JSON by default (and support
--jsonlwhen streaming). - JSON schemas must be stable and versioned via additive changes only. Avoid breaking field renames/types.
- Exit codes must be reliable and consistent; errors must be structured in JSON.
- All commands that are part of the agent workflow must emit JSON by default (and support
Assume multiple concurrent agents and workers operating on the same DB at once.
- Idempotency everywhere.
- Mutating commands should accept/propagate idempotency keys and dedupe repeated requests safely.
- Tool-like operations should be safe under retries (at-least-once execution).
- Append-only truth.
- Model history as immutable events (append-only). Derive "current state" from projections.
- Prefer content-addressed artifacts to dedupe repeated outputs.
- Single-head semantics (no branching UX).
- If/when modeling an "active head" for a run/task stream, advance it with CAS/optimistic concurrency.
- On conflicts, do not ask humans. Auto-rebase/retry with budgets (attempt limits, timeouts, backoff).
- Crash-safe progress.
- Persist intent/checkpoints before side effects where possible, and always record completion/failure.
- Resume must be deterministic: reconstruct from persisted state, not in-memory agent context.
For comprehensive examples, see Operator Guide.
cmd/vybe/main.go
↓
internal/commands/ # Cobra CLI layer (parse flags, call actions)
↓
internal/actions/ # Business logic (orchestrate store calls, build packets)
↓
internal/store/ # SQLite persistence + migrations (transactions, retry, CAS)
internal/app/ # Config loading, DB init, settings
internal/output/ # JSON output formatting
internal/models/ # Domain types shared across layers
internal/testutil/ # CLI test helpers for integration tests
Layers: Commands → Actions → Store
Models: internal/models/ (domain types shared across layers)
| Module | Key Operations |
|---|---|
task.go |
Create, start, close, set-status |
memory.go |
Set, get, list, delete, GC with TTL parsing |
artifact.go |
Add, get, list by task |
resume.go |
Resume with options, brief building, prompt assembly |
project.go |
Create, focus, get, list, delete |
push.go |
Atomic batch (event + memory + artifacts + status) |
session.go |
Auto-summarize events, auto-prune archived events |
- Keep diffs small and reviewable. Match existing patterns in
internal/commands,internal/actions,internal/store. - Prefer Go stdlib; use
context.Contextat boundaries; wrap errors with%w. - Keep DB mutations transactional; avoid partial writes. Use optimistic concurrency where contention is expected.
- Tests:
- If you change behavior or output, add/update tests.
- Prefer integration tests for resume/concurrency semantics and retry/idempotency behavior.
| Pattern | Implementation |
|---|---|
| ID generation | {type}_{unix_nano}_{random_hex} (e.g., task_1234567890_a3f9) |
| Idempotency | --request-id (optional; auto-generated req_<nano>_<hex> when omitted) + idempotency table; replay original result on duplicate request-ids |
| Optimistic concurrency | version columns on tasks/agent_state; CAS updates with retry |
| Monotonic cursor | UPDATE agent_state SET last_seen_event_id = MAX(last_seen_event_id, ?) |
| Retry logic | RetryWithBackoff() for all DB ops; exponential backoff on SQLITE_BUSY |
| Type inference | Memory values auto-detect: string, number, boolean, json, array |
| Event archiving | Summarize + archive old events; auto-prune archived |
| Session management | Auto-summarize events, auto-prune archived events |
| Project isolation | Project-scoped tasks, memory, events with focus tracking |
| State mutation fixes | Trace every caller that depends on current clearing/preserving/defaulting behavior before changing it; a fix for one edge case that breaks the normal path is worse than the original bug |
| Zero-value filter trap | When a parameter's zero value (0, "") is a valid domain value, use a sentinel (-1, pointer) to mean "no filter"; see ListTasks priorityFilter |
| Read-outside-tx TOCTOU | If a write tx depends on data read outside it, the read is stale under concurrency; read inside the tx or verify with CAS |
| Idempotent terminal conditions | "Not found" on delete and "already exists" on create are successes; returning errors poisons the idempotency record |
| Graph traversal naming | Node-count limits vs depth limits protect against different shapes; name the limit for what it actually counts |
| UTF-8 truncation | Never slice strings by byte index when storing/serializing; use []rune or unicode/utf8 |
| Token-budget prompt | Variable sections (memory, prompts, events, reasoning) share a 1500-token budget filled by priority; fixed sections always included. estimateTokens uses utf8.RuneCountInString / 4 |
| Memory access tracking | GetMemory increments access_count and updates last_accessed_at on every read (best-effort) |
| Pin stickiness | UpsertMemoryTx uses pinned = CASE WHEN excluded.pinned = 1 THEN 1 ELSE pinned END — a subsequent memory set without --pin cannot clear a pinned flag. Only memory pin --unpin can. Protects durable strategic memory from being unpinned by incidental writes |
| Resource handle sharing | N sequential ops on the same DB/file → open once, share handle; don't pay setup/migration/lock N times |
| SQL CASE NULL semantics | ELSE NULL clears; ELSE column_name preserves — opposite meanings; trace caller expectations |
| Test the inverse of fixes | After fixing an edge case, verify the happy path still works; most regressions break the normal path |
| No mutable package globals | Pass shared state explicitly or embed in a struct; package globals couple tests and concurrent callers |
| Boundary validation | Cap inputs (string length, array size, numeric range) at the store/action boundary; trust internally |
| Memory kind classification | Use --kind=directive for imperative behavioral rules (rendered first in brief under === Directives ===, value-only). Use --kind=fact (default) for key=value claims (rendered under === Facts ===). Kind is non-sticky on upsert — a new write overwrites it; only --pin is sticky-upward |
| Per-kind half-life decay | memory.half_life_days nullable; brief formula falls back to kind defaults (directive→1e9, lesson→14, fact→90); pinned sorts first so formula is only tiebreaker |
Deterministic 5-rule system (in internal/store/resume.go):
- Keep current focus if
in_progress1.5. Keep current focus ifblockedand not failure-blocked (non-failure blocked tasks stay until manually resolved) - Check deltas for
task_assignedevents - Resume old focus if unblocked
SELECThighest-priority pending task — whenfocus_project_idis set, prefer project-scoped tasks first, then fall through to global- Return empty if no work available
{
"task": {...}, // Focus task (null if none)
"project": {...}, // Focus project (null if none)
"relevant_memory": [...], // global + task-scoped + project-scoped (NOT agent-scoped)
"recent_events": [...], // Last 20 events for task
"artifacts": [...] // Files linked to task
}When focus_project_id is set, project-scoped memory is filtered to that project only.
When unset, all project-scoped memory is included.
Resume vs Peek:
vybe resume: Fetch deltas + build brief + advance cursor atomicallyvybe resume --peek: Build brief without cursor advancement (idempotent read)
| Table | Purpose |
|---|---|
events |
Append-only continuity log (id, kind, agent_name, task_id, message, metadata) |
tasks |
Mutable task definitions with optimistic concurrency (id, title, status, priority, blocked_reason, project_id, version) |
agent_state |
Cursor position + focus tracking per agent (last_seen_event_id, focus_task_id, focus_project_id) |
memory |
Scoped KV storage with TTL (scope: global/project/task/agent); unique constraint on (scope, scope_id, key) |
artifacts |
Files/outputs linked to tasks (task_id, event_id, file_path) |
idempotency |
Request deduplication (agent_name + request_id composite PK) |
projects |
Project metadata (id, name, metadata, created_at) |
Note: 20 migration files (sequence numbers have gaps from removed migrations, highest is 23); task claiming and retrospective jobs were added then removed.
SQLite Config: WAL mode, busy_timeout=5000ms, synchronous=NORMAL, foreign_keys=ON
SQLite CRITICAL: Never issue db.Query* while a parent rows cursor is open on the same *sql.DB. SQLite single-connection tests deadlock silently. Always: scan into slice, close rows, THEN do follow-up queries.
Vybe uses two complementary concurrency mechanisms. They solve different problems:
| Mechanism | What it does | What it prevents |
|---|---|---|
Transactions (Transact()) |
Pessimistic write serialization (SQLite WAL = one writer at a time) | Partial writes, torn state |
CAS versioning (WHERE version = ?) |
Optimistic read-modify-write safety | Silent overwrites when two agents read the same version, compute independently, and both try to write |
Transactions alone don't prevent read-modify-write races because the read and the decision to write can span different transactions (or different CLI invocations). CAS runs inside transactions — it's not an alternative to them.
Implementation surface:
versioncolumn ontasksandagent_stateErrVersionConflictsentinel errorRetryWithBackoff()handles bothSQLITE_BUSYand version conflictsRunIdempotentWithRetry()adds configurable retry with conflict predicate
- Prefer fixing the code over adding
//nolint. - If suppression is required, scope it to explicit rules (
//nolint:gosec) with a short reason. - Do not use blanket suppressions (
//nolintwithout rule names). - Remove stale suppressions when touching a file.
- Treat
gosecsuppressions as trust-boundary declarations; reason must state why input is trusted.
gofmt -w ./cmd/vybe ./internal
go vet ./...
go build ./...Core: Complete
- Schema + 20 migrations (including cleanup of deprecated features)
- All CRUD operations (tasks, events, memory, artifacts, agent state, projects)
- Idempotency system with replay
- Resume/brief with deterministic focus selection
- Project operations (create, focus, delete, isolation)
- Session management (auto-summarize events, auto-prune archived events)
- Event archiving and summarization
- 50 test files across all layers
Known Gaps:
- No FK constraint on
tasks.project_idoragent_state.focus_project_id— app layer validates - Event guardrails enforced centrally in
store.InsertEventTx/store.ValidateEventPayload:kindmax 128 charsagent_namemax 128 charsmessagemax 4096 charsmetadatamax 16384 chars + must be valid JSON when present
- Expired memory is cleaned via
vybe memory gcbut has no automatic scheduled cleanup - Task status transitions are intentionally unrestricted for agent flexibility (any status → any status). The
blocked_reasoncolumn is free-form; failure blocks use the"failure:<reason>"prefix convention. Resume Rule 1.5 keeps blocked focus tasks unlessblocked_reasonstarts with"failure:"
Claude Code is integrated with vybe via hooks. The system automatically:
- SessionStart: Runs
vybe resumeand injects focus task + memory into context - UserPromptSubmit: Logs user prompts for cross-session continuity; emits a rich brief only on explicit trigger words (
brief me,status, etc.) — no per-turn task reminder is injected on ordinary prompts - PostToolUseFailure: Logs failed tool calls for recovery context
- TaskCompleted: Logs task completion lifecycle signals
- PreCompact: Performs checkpoint maintenance
- SessionEnd: Performs best-effort checkpoint maintenance
- Commits: Logs git commits as vybe events
Hook registry is externalized to ~/.config/vybe/hooks.json (editable; vybe hook export emits the current manifest). Run vybe init to write the default manifest on first install.
When working on multi-step tasks, proactively use vybe for durable state:
# Close the focus task (status + optional note) in one atomic call
vybe done <task_id> --note "<summary>"
# Block on failure (resume skips failure-blocked tasks)
vybe block <task_id> --reason "<why>" --failure
# Log progress
vybe note <task_id> "<message>"
# Store a memory (omit --request-id; auto-generated)
vybe remember "<key>=<value>" --scope task --scope-id <task_id>
# Read current focus without advancing the cursor
vybe focus
# Atomic multi-op batch still available when you need it
vybe push --json '{"task_id":"<id>","event":{"kind":"progress","message":"..."},"task_status":{"status":"completed"}}'Omit --request-id — vybe auto-generates one. Pass a stable id only when retrying the same logical op.
When a plan is approved via ExitPlanMode, create vybe tasks for each implementation step:
vybe task create --agent=claude --title="Step 1: ..." --desc="..." --request-id=plan_step_1_$(date +%s)
vybe task create --agent=claude --title="Step 2: ..." --desc="..." --request-id=plan_step_2_$(date +%s)The focus task from vybe resume is your primary work item. When starting work:
- Check the brief for context (task, memory, events, artifacts)
- Use
vybe task beginto claim and mark in_progress - Log progress events as you work
- Set status to completed when done — next resume auto-advances to next task
| Variable | Default | Purpose |
|---|---|---|
VYBE_DB_PATH |
~/.config/vybe/vybe.db |
Override database file location |
VYBE_AGENT |
(none) | Default agent identity for commands (persistent fallback below it: config.yaml: default_agent) |
VYBE_REQUEST_ID |
(none) | Default idempotency key for mutations |
VYBE_BUSY_TIMEOUT_MS |
5000 |
SQLite busy_timeout override (ms) |
VYBE_PRETTY_JSON |
unset | Human-readable JSON output formatting |
- DB path precedence:
--db-path>VYBE_DB_PATH>config.yaml: db_path>~/.config/vybe/vybe.db - Agent identity:
--agentflag →VYBE_AGENTenv →config.yaml: default_agent(required for most commands; resolve via this precedence) - Idempotency:
--request-idorVYBE_REQUEST_IDfor safe retries (optional; auto-generated asreq_<nano>_<hex>when omitted, giving at-least-once) - New features follow the idempotent action pattern:
store.*Tx→actions.RunIdempotent→commands - In
RunIdempotent*closures, usetx.Query*notdb.Query*— SQLite single-connection tests deadlock silently - Task JSON hydration:
CreateTaskTx,getTaskByQuerier,ListTasksmust stay in sync when adding columns - Command wiring:
internal/commands/root.go - Claude Code hooks use snake_case stdin fields (
session_id,hook_event_name); SessionStartsourcematcher:startup|resume|clear|compact - Command surface:
artifacts,block,done,doctor,events,focus,hook(install, uninstall, export),init,memory(set, get, list, delete, gc, pin),note,push,remember,resume(--peek, --focus, --project-dir, --limit),schema,status(--check),task(create, begin, get, list, set-status),upgrade - Valid task statuses:
pending,in_progress,completed,blocked - After code changes: rebuild binary and update symlink:
go build -o vybe ./cmd/vybe && ln -sf "$(pwd)/vybe" ~/go/bin/vybe