# Getting Started

# ZettelControl Documentation

**ZettelControl** (`ztlctl`) is a local knowledge operating system for human users and AI agents. It separates agent-assisted capture and synthesis from human-led enrichment so research workflows and knowledge-garden workflows stay distinct — and coexist without interference.

ztlctl stores durable notes, references, and tasks as markdown files, indexes them in SQLite, and exposes them through a CLI, an MCP server, and generated workflow assets.

## What Makes ztlctl Different

- **Local-first SQLite** — your vault is a directory of markdown files backed by a single `.ztlctl/ztlctl.db`; no cloud account required
- **Graph-native** — every note, reference, and task is a node; links and reweave scores are edges; graph traversal and structural analysis are first-class commands
- **Agent-native MCP** — 59 MCP tools, 6 resources, and 4 prompts let AI agents query, create, and navigate your vault without custom glue code
- **Plugin ecosystem** — pluggy-based hook system with a stable versioned API; extend via local plugins or published packages

## For Knowledge Workers

Build and tend a knowledge vault using the CLI and Obsidian.

- [Quick Start](quickstart.md) — your first vault in 5 minutes
- [Tutorial](tutorial.md) — step-by-step guide to building a knowledge vault
- [Core Concepts](concepts.md) — content types, lifecycle states, vault structure, knowledge graph
- [Knowledge Paradigms](paradigms.md) — capture/synthesis and enrichment, mapped to Zettelkasten, second-brain, and garden approaches
- [Best Practices](best-practices.md) — workflow patterns and anti-patterns

## For Developers and Plugin Authors

Extend ztlctl or integrate it into your tooling.

- [Development](development.md) — architecture, ActionRegistry, and local setup
- [Plugin Authoring](plugin-guide.md) — hookspecs, plugin lifecycle, publishing
- [API Reference](api-reference.md) — auto-generated from source
- [MCP Server](mcp.md) — 59 tools, resources, prompts, and client export

## For AI Agents

Consume and operate a ztlctl vault from an agentic context.

- [agents.md](agents.md) — machine-readable system manual: tool inventory, ID formats, state machines, error codes
- [llms.txt](llms.txt) — full documentation corpus for agent ingestion
- [Agentic Workflows](agentic-workflows.md) — sessions, context assembly, batch operations, scripting

## Quick Links

| Page | Purpose |
|------|---------|
| [Installation](installation.md) | pip, uv, Homebrew, and dev install |
| [Quick Start](quickstart.md) | First vault in 5 minutes |
| [Command Reference](commands.md) | All CLI commands, options, and filters |
| [Configuration](configuration.md) | `ztlctl.toml` settings and environment variables |
| [Troubleshooting](troubleshooting.md) | Common issues and solutions |


---

# Installation

## System Requirements

- **Python 3.13+** (required — `requires-python = ">=3.13"`)
- SQLite with FTS5 support (included in all standard Python 3.13 builds)
- Operating system: Linux, macOS, or Windows

## Install Methods

### pip

```bash
pip install ztlctl
```

### uv (recommended for isolated installs)

```bash
uv tool install ztlctl
```

`uv tool install` places `ztlctl` on your PATH as an isolated tool — no virtual environment management needed.

### pipx

```bash
pipx install ztlctl
```

### Homebrew (macOS)

```bash
brew tap ThatDev/ztlctl
brew install ThatDev/ztlctl/ztlctl
```

The Homebrew formula installs the base CLI. Optional extras (MCP support, semantic search) are not available through Homebrew — install them via pip or uv after the base install.

## Optional Extras

Install extras alongside the base package to unlock additional capabilities.

### MCP Server Support

For [Model Context Protocol](mcp.md) integration with AI clients like Claude Desktop:

```bash
pip install ztlctl[mcp]
```

### Semantic Search

For vector-based similarity search using sentence-transformers:

```bash
pip install ztlctl[semantic]
```

### Community Detection Algorithms

For advanced graph algorithms (Leiden community detection):

```bash
pip install ztlctl[community]
```

### All Extras

```bash
pip install ztlctl[mcp,semantic,community]
```

## Verify Installation

```bash
ztlctl --version
```

Expected output:

```
ztlctl, version 1.16.0
```

Run a help check to confirm all commands are registered:

```bash
ztlctl --help
```

## Development Install

To contribute to ztlctl or run the test suite locally:

```bash
git clone https://github.com/ThatDevStudio/ztlctl.git
cd ztlctl
uv sync --group dev
```

This installs all runtime dependencies plus the dev group (pytest, mypy, ruff, pre-commit, mkdocs).

Verify the dev install:

```bash
uv run ztlctl --version
uv run pytest --tb=short -q
```

## Upgrading

```bash
# pip
pip install --upgrade ztlctl

# uv tool
uv tool upgrade ztlctl

# Homebrew
brew upgrade ThatDev/ztlctl/ztlctl
```

## Next Steps

Run `ztlctl init` to create your first vault, then follow the [Quick Start](quickstart.md) guide.


---

# Quick Start

## Research Project Quick Start

Set up a knowledge vault for a focused research project. This workflow covers initialization, source capture, note creation, graph linking, and session tracking.

```bash
# 1. Initialize a new vault
mkdir my-research && cd my-research
ztlctl init --name my-research --topics "python,architecture,devops"
```

The init wizard prompts for vault name, profile, agent tone, and topics when run interactively. Pass `--no-interact` to skip all prompts and use defaults.

```bash
# 2. Start a research session
ztlctl session start "Learning Python async patterns"
```

The session creates a coordination log (`LOG-0001`) and anchors all subsequent work to the topic context. Session close triggers reweave, orphan sweep, and integrity check automatically.

```bash
# 3. Capture a source reference
ztlctl create reference "Python asyncio docs" \
  --url "https://docs.python.org/3/library/asyncio.html" \
  --tags "lang/python,concept/concurrency"
```

References start at `captured` status. Each reference gets a content-hash ID (`ref_XXXXXXXX`) derived from its title.

```bash
# 4. Create synthesis notes as understanding develops
ztlctl create note "Asyncio Event Loop" \
  --tags "lang/python,concept/concurrency" \
  --topic python

ztlctl create note "Async vs Threading Trade-offs" \
  --tags "lang/python,concept/concurrency" \
  --topic python
```

Notes start at `draft` status and progress to `linked` (1+ outgoing links) and `connected` (3+ outgoing links) as reweave adds graph edges. Each note gets a content-hash ID (`ztl_XXXXXXXX`).

```bash
# 5. Track tasks that emerge from research
ztlctl create task "Refactor API to use async" \
  --priority high

# 6. Run reweave to discover connections
ztlctl reweave run
```

Reweave scores note pairs on 4 signals — BM25 lexical similarity, Jaccard tag overlap, graph proximity, and topic match — and adds edges between related content.

```bash
# 7. Search your knowledge
ztlctl query search "async patterns" --rank-by relevance

# 8. Close the session
ztlctl session close --summary "Mapped async patterns, identified refactoring task"
```

Session close runs cross-session reweave, orphan sweep, and an integrity check. No manual cleanup step needed.

---

## Daily Notes Quick Start

Use ztlctl for daily note capture without formal session tracking. This workflow suits knowledge workers who capture continuously and review periodically.

```bash
# 1. Initialize a vault for daily notes
mkdir knowledge-base && cd knowledge-base
ztlctl init --name knowledge-base --profile core --tone minimal --no-interact
```

```bash
# 2. Capture notes as ideas arrive
ztlctl create note "Distributed systems CAP theorem" \
  --tags "systems/distributed,concept/consistency"

ztlctl create note "Event sourcing vs CQRS" \
  --tags "systems/architecture,pattern/cqrs"
```

```bash
# 3. Search and retrieve
ztlctl query search "distributed consistency"

# Filter by type or tag
ztlctl query search "architecture" --type note --tag "systems/architecture"
```

```bash
# 4. Review your work queue
ztlctl query work-queue
```

The work queue scores all actionable items (tasks, stale notes) by priority, impact, and staleness — presented in the order most worth your attention.

```bash
# 5. Connect notes manually or via reweave
ztlctl reweave run --dry-run   # preview suggestions without committing
ztlctl reweave run             # apply suggestions
```

```bash
# 6. Check vault health
ztlctl check --integrity
```

---

## Next Steps

- [Tutorial](tutorial.md) — full step-by-step guide to building a knowledge vault
- [Core Concepts](concepts.md) — content types, lifecycle states, ID patterns, vault structure
- [Knowledge Paradigms](paradigms.md) — capture/synthesis vs enrichment workflows
- [Configuration](configuration.md) — `ztlctl.toml` settings for vault customization


---

# User Guide

# User Guide

ztlctl's User Guide is for knowledge workers building personal knowledge systems — whether you're a researcher, writer, product manager, or student. These guides show you how to think in zettelkasten, capture ideas, build connections, and retrieve knowledge on demand.

**Not sure where to start?** Try the [Quick Start](../quickstart.md) or the [Tutorial](../tutorial.md).

## In This Guide

| Page | What it covers |
|------|----------------|
| [Tutorial](../tutorial.md) | Hands-on walkthrough: create notes, link them, run a session |
| [Core Concepts](../concepts.md) | Content types, lifecycle states, and vault structure |
| [Knowledge Paradigms](../paradigms.md) | Second-brain vs knowledge garden — which fits your workflow |
| [Obsidian Starter Kit](../obsidian.md) | Set up Obsidian as a visual frontend for your vault |
| [Built-in Plugins](../plugins.md) | Git and Reweave plugin guides — config, triggers, and scenarios |
| [Agentic Workflows](../agentic-workflows.md) | Orchestrate AI agents with ztlctl as their note substrate |
| [Command Reference](../commands.md) | Every CLI command and flag |
| [Configuration](../configuration.md) | Config file options, vault settings, and environment variables |
| [Troubleshooting](../troubleshooting.md) | Common problems and solutions |


---

# Tutorial: Building Your Knowledge Vault

This tutorial walks through creating and managing a knowledge vault from capture through enrichment. Every command example is verified against the Click command source.

## Step 1: Initialize Your Vault

```bash
ztlctl init --path research-vault --name "Research Notes" --profile obsidian --topics "ml,systems,papers"
cd research-vault
```

This creates the directory structure, config file, SQLite database, generated identity files, and the selected workspace scaffold. With `--profile obsidian`, init also writes the first-party Obsidian starter kit, creates the human-owned `garden/` layer, and prints a checklist for installing the curated Obsidian community plugins. After that scaffold, `.obsidian/` is yours to customize in Obsidian.

**Options:**
- `--path TEXT` — Vault directory (default: current directory)
- `--name TEXT` — Vault display name
- `--profile TEXT` — Workspace profile used by initialization and self/workflow generation; `core` is always available and installed profile ids are discovered dynamically
- `--tone [research-partner|assistant|minimal]` — Agent personality for self/ files
- `--topics TEXT` — Comma-separated topic directories
- `--no-workflow` — Skip workflow template setup

!!! warning "Anti-Pattern: Interactive init without --no-interact"
    Running `ztlctl init` in a scripted or CI environment will block on interactive prompts unless you pass `--no-interact`. Always use `--no-interact` in automation and provide all required flags explicitly.

## Step 2: Capture Knowledge

**Create a note** — your primary unit of knowledge:

```bash
ztlctl create note "Transformer Architecture" \
  --tags ml/transformers --tags concept/architecture \
  --topic ml
```

**Create a reference** — capture an external source:

```bash
ztlctl create reference "Attention Is All You Need" \
  --url "https://arxiv.org/abs/1706.03762" \
  --subtype article \
  --tags ml/transformers --tags papers/seminal
```

**Ingest text directly** — useful when an agent or another tool already has the source text:

```bash
ztlctl ingest text "Transformer reading notes" --target-type reference
```

**Quick capture with garden seed** — when you want minimal friction:

```bash
ztlctl garden seed "Idea: attention mechanisms for code review" \
  --tags "ml/attention" --topic ml
```

Seeds start at `seed` maturity and can grow to `sprout` then `evergreen` as you develop them.

!!! warning "Anti-Pattern: Creating notes without tags"
    Notes without tags are harder to find, harder to reweave, and harder to group into topic packets. Tags drive both the tag-Jaccard reweave signal and the `query list --tag` filter. Always add at least one domain-scoped tag when creating a note or reference.

## Step 3: Work with Tasks

```bash
ztlctl create task "Read BERT paper" --priority high --impact high --effort low
ztlctl create task "Implement attention visualization" --priority medium
```

View your prioritized work queue:

```bash
ztlctl query work-queue
```

Tasks are scored by priority x impact / effort and presented in actionable order.

## Step 4: Connect Knowledge

**Discover and apply connections** — reweave analyzes all content and creates links above the score threshold:

```bash
ztlctl reweave run
```

**Dry run** to preview what would change:

```bash
ztlctl reweave run --dry-run
```

**Target a specific note:**

```bash
ztlctl reweave run --content-id ztl_a1b2c3d4
```

Reweave uses a 4-signal scoring algorithm:
1. **BM25** (35%) — lexical similarity between content bodies
2. **Tag Jaccard** (25%) — tag overlap between items
3. **Graph Proximity** (25%) — existing network distance
4. **Topic Match** (15%) — shared topic directory

!!! tip
    The Reweave plugin runs automatically after every `create note` and `create reference` operation, so you rarely need to run `ztlctl reweave run` manually. See [Built-in Plugins](plugins.md) for Reweave plugin configuration.

## Step 5: Query and Explore

**Full-text search:**

```bash
ztlctl query search "transformer attention" --rank-by relevance
ztlctl query search "python async" --rank-by recency --type note
ztlctl query search "architecture" --rank-by graph  # PageRank-boosted
ztlctl query search "architecture" --rank-by review # review-oriented rerank
ztlctl query search "oauth" --rank-by garden        # enrichment-oriented rerank
```

**List with filters:**

```bash
ztlctl query list --type note --status draft
ztlctl query list --tag "ml/transformers" --sort recency
ztlctl query list --maturity seed --since 2025-01-01
ztlctl query list --include-archived --sort title
```

**Get a specific item:**

```bash
ztlctl query get ztl_a1b2c3d4
```

**Decision support** — aggregate context for a decision:

```bash
ztlctl query decision-support --topic architecture
ztlctl query packet architecture --mode learn
ztlctl query draft architecture --target note
```

## Step 6: Analyze the Graph

**Find related content** via spreading activation:

```bash
ztlctl graph related ztl_a1b2c3d4 --depth 2 --top 10
```

**Discover topic clusters:**

```bash
ztlctl graph themes
```

**Find the most important nodes** (PageRank):

```bash
ztlctl graph rank --top 20
```

**Find the shortest path** between two ideas:

```bash
ztlctl graph path ztl_a1b2c3d4 ztl_e5f6g7h8
```

**Find structural gaps** — orphan notes with no connections:

```bash
ztlctl graph gaps --top 10
```

**Find bridge nodes** — key connectors between clusters:

```bash
ztlctl graph bridges --top 10
```

## Step 7: Update and Evolve

**Update metadata:**

```bash
ztlctl update ztl_a1b2c3d4 --title "New Title" --tags new/tag
ztlctl update ztl_a1b2c3d4 --status linked
ztlctl update ztl_a1b2c3d4 --maturity sprout  # Grow a garden note
```

**Archive** — soft-delete that preserves graph edges:

```bash
ztlctl archive ztl_a1b2c3d4
```

**Supersede a decision:**

```bash
ztlctl supersede ztl_old_decision ztl_new_decision
```

!!! warning "Anti-Pattern: Updating status directly without valid transitions"
    ztlctl enforces lifecycle rules: a note in `draft` status cannot jump to `connected` in a single update. Attempting invalid transitions fails with `INVALID_TRANSITION`. Use the work queue (`ztlctl query work-queue`) to understand the right next action for each item.

## Step 8: Export and Share

**Export markdown** — portable copy of all content:

```bash
ztlctl export markdown ./export/
```

**Generate indexes** — type and topic groupings:

```bash
ztlctl export indexes ./indexes/
```

**Export the knowledge graph:**

```bash
ztlctl export graph --format dot --output graph.dot  # For Graphviz
ztlctl export graph --format json --output graph.json # For D3.js / vis.js
```

**Export a dashboard** for enrichment work:

```bash
ztlctl export dashboard ./dashboard/ --viewer obsidian
```

This dashboard is an external review workbench. It helps you review machine-layer work queues, stale/orphan signals, and topic dossiers before doing human-led garden work in Obsidian.

This export includes:

- `dashboard.md`
- `review-queue.json`
- `decision-queue.json`
- `garden-backlog.json`
- `topic-review-summary.json`
- `topics/<topic>.md` and `topics/<topic>.json` dossiers for the busiest recent topics

## Step 9: Maintain Integrity

**Check vault health:**

```bash
ztlctl check check
```

**Auto-fix detected issues:**

```bash
ztlctl check fix
ztlctl check fix --level aggressive  # More thorough repairs
```

**Full rebuild** — re-derive the entire database from files:

```bash
ztlctl check rebuild
```

**Rollback** to the last backup:

```bash
ztlctl check rollback
```

!!! warning "Anti-Pattern: Skipping regular integrity checks"
    Running `ztlctl check check` before large operations (bulk import, manual file edits) prevents silent corruption. Regular checks are especially important after manually editing `.md` files outside ztlctl, as frontmatter changes can desync the database.

## Next Steps

- [Core Concepts](concepts.md) — Deeper understanding of content types and lifecycle
- [Command Reference](commands.md) — Full flag reference for every command
- [Agentic Workflows](agentic-workflows.md) — Using ztlctl with AI agents
- [Knowledge Paradigms](paradigms.md) — Capture/synthesis and enrichment across those paradigms


---

# Core Concepts

## Content Types

ztlctl manages three durable authored artifact types plus one operational session type:

| Type | Purpose | Initial Status | ID Format |
|------|---------|---------------|-----------|
| **Note** | Ideas, knowledge, decisions | `draft` | `ztl_XXXXXXXX` |
| **Reference** | External sources (articles, tools, specs) | `captured` | `ref_XXXXXXXX` |
| **Task** | Actionable work items | `inbox` | `TASK-NNNN` |
| **Log** | Session coordination and session entries | `open` | `LOG-NNNN` |

Notes, references, and tasks are the file-first durability contract. Sessions, session logs, generated `self/` files, and event/WAL state are internal or generated mechanisms that can be rebuilt or regenerated.

Ingested references may also carry a durable source bundle under `sources/<reference-id>/`. That bundle is an attached source artifact for the reference, not a separate top-level knowledge item.

### A Concrete Example

A note capturing Python decorator patterns:

```
ID:     ztl_a1b2c3d4
Title:  Python Decorator Patterns
Status: linked           # computed from outgoing link count (1+ links)
Tags:   lang/python, pattern/decorator
Links:  ztl_e5f6g7h8     # links to "Asyncio Event Loop" note
Topic:  python/
File:   notes/python/ztl_a1b2c3d4.md
```

A reference capturing its source:

```
ID:     ref_deadbeef
Title:  PEP 318 — Decorators for Functions and Methods
URL:    https://peps.python.org/pep-0318/
Status: captured
Tags:   lang/python, concept/meta
File:   notes/python/ref_deadbeef.md
```

## Content Subtypes

Notes and references can be further classified:

- **Note subtypes**: `knowledge` (long-lived insight), `decision` (architectural/design choice)
- **Reference subtypes**: `article`, `tool`, `spec`
- **Garden maturity**: `seed` (raw capture) → `budding` (developing) → `evergreen` (polished)

Custom subtypes are supported via the plugin system — see [Plugin Authoring](plugin-guide.md) for details.

## ID Patterns

IDs are permanent — once generated, an ID never changes.

| Content Type | Format | Generation |
|---|---|---|
| Note | `ztl_XXXXXXXX` | SHA-256 of normalized title (8 hex chars) |
| Reference | `ref_XXXXXXXX` | SHA-256 of normalized title (8 hex chars) |
| Task | `TASK-NNNN` | Sequential counter from DB (minimum 4 digits) |
| Log | `LOG-NNNN` | Sequential counter from DB (minimum 4 digits) |

Content-hash IDs (notes, references) are deterministic — creating a note with the same title twice produces the same ID. Sequential IDs (tasks, logs) increment atomically.

## Lifecycle States

Each content type follows a defined state machine. Transitions are enforced — invalid transitions are rejected.

### Note States

```
draft → linked → connected
```

| State | Condition |
|---|---|
| `draft` | Fewer than 1 outgoing link |
| `linked` | 1 or more outgoing links |
| `connected` | 3 or more outgoing links |

Note status is computed automatically from structural properties — it is never set directly by CLI command. Run `ztlctl reweave run` to add links and advance note status.

### Reference States

```
captured → annotated
```

| State | Meaning |
|---|---|
| `captured` | Source stored, not yet annotated |
| `annotated` | Annotations and links added |

### Task States

```
inbox → active → done
             ↕
           blocked
inbox → dropped
active → dropped
blocked → dropped
```

| State | Meaning |
|---|---|
| `inbox` | Captured, not yet started |
| `active` | In progress |
| `blocked` | Waiting on something |
| `done` | Completed (terminal) |
| `dropped` | Abandoned (terminal) |

### Log States (Sessions)

```
open ↔ closed
```

Sessions are reopenable — `ztlctl session reopen LOG-NNNN` transitions `closed` back to `open`.

### Decision States (Note Subtype)

```
proposed → accepted → superseded
```

Decision notes track architectural or design choices through their full lifecycle.

## Vault Structure

```
my-vault/
├── ztlctl.toml          # Configuration
├── .ztlctl/
│   └── ztlctl.db        # SQLite index, session state, graph data, FTS5
├── self/
│   ├── identity.md      # Generated agent identity
│   └── methodology.md   # Generated agent methodology
├── notes/
│   ├── python/
│   │   ├── ztl_a1b2c3d4.md    # Note: Python Decorator Patterns
│   │   └── ref_deadbeef.md    # Reference: PEP 318
│   └── architecture/
│       └── ztl_e5f6g7h8.md    # Note: Asyncio Event Loop
├── sources/
│   └── ref_deadbeef/
│       ├── bundle.json         # Captured source metadata
│       └── normalized.md       # Normalized source text
└── ops/
    └── tasks/
        └── TASK-0001.md
```

`ztlctl check --rebuild` reconstructs durable authored artifacts and derived indexes from the markdown files. It does not treat session rows or other operational state as part of the file-first guarantee.

## Tags

Tags use a `domain/scope` format for structured categorization:

```bash
--tags "lang/python"          # domain=lang, scope=python
--tags "concept/concurrency"  # domain=concept, scope=concurrency
--tags "status/wip"           # domain=status, scope=wip
```

Unscoped tags (e.g., `python`) work but generate a warning — the `domain/scope` format enables powerful filtering via `--tag` in search and list commands.

## Knowledge Graph

Every content item is a node. Edges are created through:

- **Frontmatter links**: Explicit `links:` in YAML frontmatter
- **Wikilinks**: `[[Note Title]]` references in body text
- **Reweave**: Automated link discovery using 4-signal scoring — BM25 lexical similarity, Jaccard tag overlap, graph proximity, and topic match

### Graph Commands

```bash
ztlctl graph related ztl_a1b2c3d4   # find notes related to a specific note
ztlctl graph path ztl_a1b2 ztl_c3d4 # shortest path between two notes
ztlctl graph rank                    # PageRank-based importance ranking
ztlctl graph gaps                    # identify weakly connected clusters
ztlctl graph bridges                 # find structural bridge nodes
```

See the [Command Reference](commands.md) for all graph commands and options.

## Relationships Between Concepts

The content type, lifecycle, and graph systems work together:

1. You create a note (`draft`) or reference (`captured`)
2. Reweave scores the note against existing content and adds graph edges
3. As edges accumulate, note status advances: `draft` → `linked` → `connected`
4. Sessions group work into coordination logs and trigger enrichment on close

For the workflow patterns that build on these primitives, see [Knowledge Paradigms](paradigms.md).


---

# Knowledge Paradigms

ztlctl uses a two-layer model instead of collapsing every knowledge method into one workflow. The first layer — capture and synthesis — is where agents and humans work together to collect sources, build references, and develop rough understanding. The second layer — enrichment — is where the human user takes over to deepen, reorganize, and mature that foundation. Second-brain and knowledge-garden approaches are not competing methods: they map naturally to these two layers, and ztlctl supports both simultaneously.

## Second-Brain vs Knowledge Garden

### At a Glance

| Dimension | Second-Brain (Capture Layer) | Knowledge Garden (Enrichment Layer) |
|-----------|------------------------------|--------------------------------------|
| Primary question | "What did I learn?" | "What do I understand?" |
| Capture style | Broad, fast, reference-driven | Selective, slow, insight-driven |
| Content types | Notes, references, tasks, ingested sources | Garden seeds, budding notes, evergreen notes |
| Organization | Topic routing, session batching | Maturity tiers, backlink structure |
| Enrichment | Automated (reweave, sessions) | Human-led (Obsidian, dashboard review) |
| ztlctl commands | `create`, `ingest`, `session`, `query search` | `garden seed`, `update --maturity`, `export dashboard`, `query packet --mode review` |
| Agent role | Agent captures, synthesizes, triages | Agent assists conversationally, does not garden |

Both approaches coexist in a single vault. Content captured in the machine layer (`notes/`, `ops/`) is indexed, searchable, and reweaved automatically. Garden content lives in `garden/` — ztlctl never indexes or mutates that directory. The two layers complement rather than replace each other.

## Choose Your Path

**"I want to research a new technology or topic"**

Use the second-brain capture-first approach. Start a focused session, ingest sources and create synthesis notes as understanding develops, and let reweave connect the material automatically.

→ See [Scenario 1: Researching a New Technology](#scenario-1-researching-a-new-technology-second-brain-approach)

---

**"I have captured material and want to deepen my understanding"**

Use the knowledge-garden enrichment-first approach. Surface seed notes waiting for attention, review the work queue for stale or actionable items, and promote notes as understanding matures.

→ See [Scenario 2: Tending Existing Knowledge](#scenario-2-tending-existing-knowledge-knowledge-garden-approach)

---

**"I want both — capture now, tend later"**

They coexist by design. Capture in the machine layer (`notes/`, `ops/`) using `create`, `ingest`, and `session`. Tend in the garden layer (`garden/`) using Obsidian or `ztlctl update`. ztlctl never indexes or mutates `garden/` content, so the two workflows never interfere.

→ See [Scenario 3: Agent-Assisted Hybrid](#scenario-3-agent-assisted-hybrid-both-paradigms)

---

## Scenario 1: Researching a New Technology (Second-Brain Approach)

You're investigating OAuth security. Start a focused session to keep context anchored, ingest sources as you find them, and build synthesis notes as understanding develops.

```bash
# Start a focused research session
ztlctl agent session start "oauth security"
```

The session creates a coordination log and anchors all subsequent work to the `oauth security` topic context.

```bash
# Ingest sources as references
ztlctl ingest text "RFC 6749 notes" --stdin --as reference --tags "auth/oauth"
```

Each ingested source becomes a `reference` in the machine layer with a stored source bundle under `sources/`.

```bash
# Create synthesis notes as understanding develops
ztlctl create note "OAuth 2.0 threat model" \
  --tags "auth/oauth,security" \
  --topic auth
```

Notes start at `draft` status and progress through `linked` → `connected` as reweave adds links.

```bash
# Build a learning packet from captured material
ztlctl query packet --topic auth --mode learn
```

The learning packet aggregates your captured references and notes into a structured context block for review or agent handoff.

```bash
# Draft a synthesis note from the packet
ztlctl query draft --topic auth --target note
```

```bash
# Close session — triggers reweave, orphan sweep, and integrity check automatically
ztlctl agent session close --summary "Mapped OAuth threat surface"
```

!!! note
    At session close, ztlctl automatically runs cross-session reweave to connect the new notes, an orphan sweep to link any isolated notes, and an integrity check. No manual step needed.

---

## Scenario 2: Tending Existing Knowledge (Knowledge-Garden Approach)

You have a vault with captured material and want to deepen your understanding of the auth topic. Surface what needs attention, review the work queue, and promote notes as understanding matures.

```bash
# Surface seed notes waiting for enrichment
ztlctl query list --maturity seed --sort recency
```

Seed notes are raw captures — garden seeds you planted but haven't developed yet.

```bash
# Review the work queue for stale or actionable items
ztlctl query work-queue
```

The work queue scores all actionable items by priority, impact, and effort, presenting them in the order most worth your attention.

```bash
# Get a review packet for the topic you're tending
ztlctl query packet --topic auth --mode review
```

The review packet surfaces stale notes, weakly connected items, and decision-support context for the topic — machine-layer signals to guide where human tending is most valuable.

```bash
# Promote a note as understanding deepens
ztlctl update ZTL-0001 --maturity budding
```

Maturity tiers — `seed` → `budding` → `evergreen` — track how thoroughly a note has been developed.

```bash
# Export a dashboard for visual review in Obsidian
ztlctl export dashboard --viewer obsidian --output ./dashboard
```

!!! note
    Garden work happens in Obsidian (the `garden/` directory) or directly via `ztlctl update`. The exported dashboard is a review workbench — machine-layer signals to guide where human tending is most valuable, not a mirror of `garden/` state.

---

## Scenario 3: Agent-Assisted Hybrid (Both Paradigms)

You're working alongside an agent. The agent captures research in the machine layer; you tend connections and deepen notes in the garden layer. Neither workflow disrupts the other.

```bash
# Agent captures research in the machine layer
ztlctl create note "Distributed consensus trade-offs" --tags "systems/consensus" --topic systems
```

```bash
# Human reviews agent-captured seeds and promotes them
ztlctl query list --maturity seed --sort recency
ztlctl update ZTL-0042 --maturity budding
```

```bash
# Human tends connections in Obsidian's garden/ layer (no ztlctl command needed)
# ztlctl does not index or mutate garden/ content
```

!!! tip
    The garden layer is entirely human-owned. Agents can assist conversationally — answering questions against captured knowledge — but they do not create or modify garden content. This separation ensures your long-form insight work stays under your control.

---

## How The Paradigms Map

| Paradigm | ztlctl role |
|----------|-------------|
| **Zettelkasten** | Durable note/reference creation, graph links, reweave, related-content traversal |
| **Second brain** | Broad capture, references, tasks, topic organization, agent-assisted retrieval |
| **Knowledge garden** | Human-led enrichment, maturity tracking, backlog review, and dashboard-guided tending from machine-layer review signals |

## What ztlctl Does Not Claim

- It does not force PARA or any other organizational method as the canonical ontology.
- It does not treat garden work as fully automatable.
- It does not treat operational session state as equivalent to durable knowledge artifacts.

## Anti-Patterns

Avoid these common mistakes when setting up a ztlctl workflow.

!!! warning "Don't mix paradigm tags without namespacing"
    Using `python` as a tag across both a second-brain research vault and a knowledge-garden vault makes retrieval ambiguous. Use namespaced tags (`lang/python`, `project/my-app`) so filters work cleanly across paradigms.

!!! warning "Don't force one paradigm for all content"
    Not every piece of knowledge needs to go through the full enrichment pipeline. Quick captures, ephemeral tasks, and reference-only notes belong in the machine layer. Reserve the garden layer for content you genuinely intend to develop into long-form insight.

!!! warning "Don't let agents garden"
    Agents operate on the machine layer (`notes/`, `ops/`). They do not create or modify content in `garden/`. Granting agents write access to garden content undermines the human-led quality guarantee that makes garden notes trustworthy.

## Intended Flow

1. Capture sources and rough synthesis quickly.
2. Use search, packets, and reweave to stabilize the foundation knowledge layer.
3. Use packets, drafts, and dashboard dossiers to turn captured evidence into reviewable work.
4. Enrich that foundation through slower, human-led garden work.

## Next Steps

- Follow the [Tutorial](tutorial.md) to build your first vault end-to-end
- Read [Core Concepts](concepts.md) for the full content type, ID format, and lifecycle state reference
- Read [Best Practices](best-practices.md) for workflow patterns and anti-pattern deep dives
- See [Agentic Workflows](agentic-workflows.md) for session lifecycle, recipe walkthroughs, and MCP integration
- See [Plugin Authoring](plugin-guide.md) to extend paradigm support with custom note types


---

# Obsidian Starter Kit

This guide walks through setting up the first-party Obsidian integration from vault init through community plugin activation. The obsidian workspace profile scaffolds a curated starter kit optimized for hybrid human-agent knowledge work. You don't need Obsidian to use ztlctl — this guide is for users who want to pair the two.

The first-party `obsidian` workspace profile scaffolds a curated Obsidian starter kit for hybrid knowledge work.

It creates:

- `.obsidian/` starter configuration scaffolded during init
- `garden/` directories and templates owned by the human after scaffold
- install and verification guidance shown during `ztlctl init --profile obsidian`

It does **not**:

- download community plugin binaries
- enable plugins automatically inside Obsidian
- index `garden/` through the ztlctl core

## Setup Walkthrough

### Scenario 1: Research Vault

Initialize a research vault with the Obsidian profile — the most common setup for academics and knowledge workers tracking literature and concepts:

```bash
ztlctl init --path research-vault --profile obsidian --name "Research Vault" --topics "ml,systems"
cd research-vault
```

Expected output:

```
✓ Vault initialized: research-vault/
✓ Obsidian starter kit scaffolded (.obsidian/, garden/)
✓ Community plugin configs written

Next steps for Obsidian:
1. Open research-vault/ in Obsidian and trust it
2. Settings → Community plugins → Browse → install: dataview, templater-obsidian,
   folder-notes, omnisearch, obsidian-book-search-plugin
3. Enable all five plugins
4. Verify settings in .obsidian/ took effect (graph view, templates path)
```

!!! note
    ztlctl writes the .obsidian/ starter files during init, then leaves them for you to customize. After the first open, .obsidian/ is yours — ztlctl won't overwrite your changes.

### Scenario 2: Team Wiki Vault

A team wiki benefits from the same profile, with topics aligned to project areas and the git plugin enabled so edits are tracked automatically:

```bash
ztlctl init --path team-wiki --profile obsidian --name "Team Wiki" --topics "architecture,ops,decisions"
cd team-wiki
```

Configure the git plugin for team use in `ztlctl.toml`:

```toml
[plugins.git]
enabled = true
batch_commits = false    # commit immediately so teammates see changes faster
auto_push = true         # push to shared remote on every operation
```

Each teammate's Obsidian instance opens the same vault directory. ztlctl's database (`/.ztlctl/`) is not shared via git — only the markdown files and `.obsidian/` config are committed. Run `ztlctl check rebuild` to re-derive the local database from the shared files when conflicts arise.

## What Gets Scaffolded

The Obsidian starter kit writes:

- `.obsidian/app.json`
- `.obsidian/appearance.json`
- `.obsidian/core-plugins.json`
- `.obsidian/community-plugins.json`
- `.obsidian/templates.json`
- `.obsidian/graph.json`
- `.obsidian/snippets/ztlctl.css`
- `.obsidian/snippets/garden-layers.css`
- plugin config files for `folder-notes`, `omnisearch`, and `obsidian-book-search-plugin`
- `garden/README.md`
- `garden/notes/`, `garden/groves/`, `garden/library/`, `garden/canvases/`, `garden/attachments/`
- `garden/templates/note.md`, `grove.md`, and `book.md`

## Curated Community Plugin Preset

The generated `.obsidian/community-plugins.json` expects you to install:

- `dataview`
- `templater-obsidian`
- `folder-notes`
- `omnisearch`
- `obsidian-book-search-plugin`

ztlctl writes config and guidance for those plugins, but it does not ship the plugin binaries.

## Why Each Community Plugin

| Plugin | Purpose in this workflow |
|--------|--------------------------|
| `dataview` | Query your ztlctl-exported dashboard as a live Obsidian database — surfaces stale notes, seed candidates, and decision queue without leaving Obsidian |
| `templater-obsidian` | Apply garden templates (note.md, grove.md, book.md) scaffolded by ztlctl init — keeps garden entries consistent |
| `folder-notes` | Makes garden/ subdirectory structure navigable in Obsidian's file explorer — groves and canvases get clickable folder notes |
| `omnisearch` | Full-text search across both ztlctl notes AND garden/ content in one Obsidian search bar |
| `obsidian-book-search-plugin` | Fetch metadata for books added to the library/ garden layer — configured to match the book.md template |

## Vault Structure

The vault contains both machine-managed paths (owned by ztlctl) and human-managed paths (owned by you):

```
research-vault/
├── ztlctl.toml          # Core config — managed by ztlctl
├── .ztlctl/             # Internal state (DB, backups) — managed by ztlctl
├── self/                # Identity and methodology files — managed by ztlctl
├── notes/               # Atomic knowledge notes — managed by ztlctl
├── ops/                 # Tasks and session logs — managed by ztlctl
├── sources/             # Ingested source bundles — managed by ztlctl
├── .obsidian/           # Obsidian workspace config — scaffolded by ztlctl, then yours
└── garden/              # Human-managed knowledge garden — never touched by ztlctl
    ├── README.md
    ├── notes/           # Garden notes (seed → sprout → evergreen)
    ├── groves/          # Topic clusters
    ├── library/         # Books and long-form sources
    ├── canvases/        # Visual mind maps
    ├── attachments/     # Images and files
    └── templates/       # note.md, grove.md, book.md
```

!!! tip
    garden/ is intentionally outside ztlctl's indexing. You can freely restructure, rename, and reorganize garden/ content without affecting ztlctl's graph or database.

## Ownership

- Core-managed paths: `ztlctl.toml`, `.ztlctl/`, `self/`, `notes/`, `ops/`
- Profile-associated scaffold surface: `.obsidian/`
- Human-managed paths: `garden/`

`ztlctl` writes the `.obsidian/` starter files during init, then leaves them for you to customize in Obsidian or by editing the files directly. `garden/` is intentionally outside default indexing and mutation. The core vault model still indexes only `notes/` and `ops/`.

## First Open in Obsidian

After `ztlctl init --profile obsidian`:

1. Open the vault in Obsidian and trust it.
2. Install the curated community plugins from Settings -> Community plugins.
3. Enable those plugins so the scaffolded config in `.obsidian/` takes effect.
4. Verify that new notes target `garden/notes`, attachments target `garden/attachments`, templates point at `garden/templates`, and both CSS snippets are enabled.

The same checklist is printed during init and stored in `garden/README.md`. `ztlctl` does not later validate or rewrite your `.obsidian/` changes.

## Using garden/ for Enrichment

After a ztlctl session, export a review dashboard and open it in Obsidian to guide garden work:

```bash
# After closing a session, generate a review workbench
ztlctl export dashboard ./dashboard/ --viewer obsidian

# Then open dashboard/ in Obsidian alongside your vault
# The dashboard shows: stale notes, orphan candidates, topic dossiers
```

The exported dashboard is a machine-layer review workbench — not a mirror of garden/. Use it to decide what to tend next, then do the tending in Obsidian's garden/ directly.

## Relationship to Export

`ztlctl export dashboard ./output/ --viewer obsidian` is an external review workbench, not part of the starter kit itself. It gives you portable markdown and JSON review artifacts for triage and topic review, but it does not export the literal `garden/` directory and it does not mirror `.obsidian/` workspace state.

## Common Pitfalls

**Sync conflicts from `.ztlctl/` in git:**
The `.ztlctl/` directory contains the SQLite database and binary backup files. Never commit it — it is written to `.gitignore` automatically during `ztlctl init`. If it ends up in git, binary merge conflicts will corrupt the database. Remove it from git tracking with `git rm -r --cached .ztlctl/`.

**Manually editing the SQLite database:**
The `.ztlctl/ztlctl.db` file is managed exclusively by ztlctl. Editing it directly with a SQLite browser bypasses frontmatter sync and breaks the filesystem-as-source-of-truth contract. If you need to correct a record, update the corresponding `.md` file and run `ztlctl check rebuild` to re-derive the database.

**Ignoring the `.ztlctl/` directory in Obsidian:**
Obsidian's file indexer will try to open `.ztlctl/` contents if not excluded. Add `.ztlctl` to Obsidian's excluded file paths in Settings → Files & Links → Excluded files to keep binary files out of Obsidian search and graph.

**Modifying `.obsidian/` config and expecting ztlctl to re-read it:**
ztlctl reads vault config from `ztlctl.toml` only. Changes to `.obsidian/app.json` or similar files are not read by ztlctl. The relationship is one-directional: ztlctl writes `.obsidian/` once at init, then Obsidian and you own it.

## Next Steps

- See [Built-in Plugins](plugins.md) for Git and Reweave plugin configuration, including git plugin setup for team vaults
- See [Agentic Workflows](agentic-workflows.md) for session lifecycle and recipe walkthroughs
- See [Configuration](configuration.md) for the full ztlctl.toml reference


---

# Built-in Plugins

ztlctl ships two built-in plugins that run automatically in the background: the Git plugin for automatic version control and the Reweave plugin for automatic link discovery. Both are enabled by default. This page explains exactly what they do, when they run, and how to configure them.

All plugin config lives under the `[plugins.<name>]` TOML key in `ztlctl.toml`. Config is validated at load time against the plugin's declared Pydantic schema (via `get_config_schema` hookspec). See [Configuration](configuration.md) for the full `ztlctl.toml` reference.

## Git Plugin

The Git plugin provides automatic version control for vault operations. When you create a note, the plugin stages the file and — depending on configuration — either commits immediately or batches the commit for session close.

### Prerequisites

!!! note
    The Git plugin requires `git` to be installed and available on your PATH. If git is not found, the plugin logs a debug message and continues silently — vault operations never fail due to git errors.

Verify git is available:

```bash
git --version
```

### What Gets Committed Automatically

Every vault operation that creates or modifies a markdown file triggers a git stage. Commit timing depends on the `batch_commits` setting (see [Configuration](#git-plugin-configuration) below).

| Action | Commit Message Format |
|--------|-----------------------|
| `ztlctl create note` | `feat: create note {id} — {title}` |
| `ztlctl create reference` | `feat: create reference {id} — {title}` |
| `ztlctl create task` | `feat: create task {id} — {title}` |
| `ztlctl update` | `docs: update {id} ({fields_changed})` |
| `ztlctl archive` | `docs: close {id} — {summary}` |
| `ztlctl session close` | `docs: session {id} — N created, N updated` |
| `ztlctl init` | `feat: initialize vault '{name}'` |

Operations that are **no-ops** for the Git plugin: `reweave run`, `session start`, `check check`, `check rebuild`. These do not stage or commit any files.

### Batch Mode vs Immediate Mode

| Mode | Behavior | When to use |
|------|----------|-------------|
| **Batch (default)** | Files are staged on each operation; one commit is made at session close | Session-based workflows — clean history with one commit per session |
| **Immediate** | Files are staged AND committed after every individual operation | Headless or no-session workflows — each note creation is its own commit |

!!! warning
    In batch mode, creating notes outside an active session stages files but never commits them — the commit trigger is `session close`. If you work without sessions, set `batch_commits = false`.

**What git status looks like in batch mode (before session close):**

```bash
$ git status
On branch develop
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   notes/ZTL-0001.md
        new file:   notes/ZTL-0002.md
        modified:   notes/ZTL-0003.md
```

After `ztlctl session close`, all staged changes are committed in one operation:

```
docs: session LOG-0001 — 2 created, 1 updated
```

### Git Plugin Configuration

In `ztlctl.toml`:

```toml
[plugins.git]
enabled = true
batch_commits = true      # true = commit at session close, false = commit immediately
auto_push = true          # push to remote on session close
auto_ignore = true        # write .gitignore during vault init
```

**All config fields** (sourced from `GitConfig` model in `config/models.py`):

| Field | Default | Meaning |
|-------|---------|---------|
| `enabled` | `true` | Enable or disable the entire plugin |
| `batch_commits` | `true` | Batch all changes into one session-close commit |
| `auto_push` | `true` | Push to remote after session-close commit |
| `auto_ignore` | `true` | Write .gitignore during `ztlctl init` |
| `branch` | `"develop"` | Target branch (informational — plugin does not enforce branch) |
| `commit_style` | `"conventional"` | Commit message format (currently only `"conventional"`) |

The auto-generated `.gitignore` excludes:

```
# ztlctl vault gitignore
.ztlctl/backups/
*.db-journal
```

### Common Scenarios

**Disable automatic commits entirely:**

```toml
[plugins.git]
enabled = false
```

**Use immediate commits (one commit per note):**

```toml
[plugins.git]
batch_commits = false
```

**Push to remote automatically on session close:**

```toml
[plugins.git]
auto_push = true
```

---

## Reweave Plugin

The Reweave plugin automatically discovers connections for new notes and references the moment they are created. It runs the full 4-signal scoring algorithm against all existing content and creates graph edges for items above the threshold.

### What It Does

When you create a note or reference, the Reweave plugin immediately calls the reweave pipeline on the new item. This means your vault's link graph is always up to date without any manual `ztlctl reweave run` call.

```bash
# When you run this:
ztlctl create note "Attention mechanisms" --tags ml/transformers

# The Reweave plugin automatically runs the equivalent of:
ztlctl reweave run --content-id ZTL-0042
# → finds related notes via BM25 + tag overlap + graph proximity + shared topic
# → creates edges for items scoring above 0.6 (default threshold)
```

### When It Runs (and When It Doesn't)

**Fires for:** `create note`, `create reference` only.

**Does not fire for:** `create task`, `update`, `archive`, `session close`, or `reweave run` (manual reweave is not re-triggered).

**Skip conditions (checked in order):**

1. The create operation failed — skip
2. The created item has no ID — skip
3. `subtype = "decision"` — skip (decision notes have strict lifecycle and must not be auto-mutated)
4. `--no-reweave` flag was passed — skip for this invocation only
5. `[reweave] enabled = false` in config — skip globally

!!! note
    Decision notes (`ztlctl create note --subtype decision`) are intentionally excluded from auto-reweave. Decision notes represent deliberate choices and must not be auto-linked by background processes. Run `ztlctl reweave run --content-id {id}` manually if you want to connect a decision note.

### 4-Signal Scoring

The reweave algorithm scores candidate links using four signals:

| Signal | Default Weight | What It Measures |
|--------|---------------|-----------------|
| BM25 lexical similarity | 0.35 | Shared vocabulary between note bodies |
| Jaccard tag overlap | 0.25 | Proportion of tags in common |
| Graph proximity | 0.25 | How closely connected via existing edges |
| Shared topic directory | 0.15 | Whether notes share a topic routing prefix |

A link is created when the combined score exceeds `min_score_threshold` (default: 0.6).

### Reweave Plugin Configuration

In `ztlctl.toml`:

```toml
[reweave]
enabled = true
min_score_threshold = 0.6   # raise to 0.75 for fewer, higher-quality links
max_links_per_note = 5      # raise for denser graphs
lexical_weight = 0.35
tag_weight = 0.25
graph_weight = 0.25
topic_weight = 0.15
```

**All config fields** (sourced from `ReweaveConfig` model in `config/models.py`):

| Field | Default | Meaning |
|-------|---------|---------|
| `enabled` | `true` | Global enable/disable for auto-reweave |
| `min_score_threshold` | `0.6` | Minimum combined score (0.0–1.0) to create a link |
| `max_links_per_note` | `5` | Maximum new links per auto-reweave run |
| `lexical_weight` | `0.35` | BM25 signal weight |
| `tag_weight` | `0.25` | Tag overlap signal weight |
| `graph_weight` | `0.25` | Graph proximity signal weight |
| `topic_weight` | `0.15` | Topic directory signal weight |

Weights do not need to sum to 1.0 — they are relative, not normalized.

### Common Scenarios

**Disable auto-reweave for a single note:**

```bash
ztlctl create note "Quick capture" --no-reweave
```

**Disable auto-reweave globally:**

```toml
[reweave]
enabled = false
```

**Tune for fewer, higher-quality links:**

```toml
[reweave]
min_score_threshold = 0.75
max_links_per_note = 3
```

**Emphasize tag-based connections (lower lexical weight):**

```toml
[reweave]
lexical_weight = 0.20
tag_weight = 0.45
```

**Run reweave manually for an existing note:**

```bash
ztlctl reweave run --content-id ZTL-0042
```

---

## Anti-Patterns

!!! warning "Anti-Pattern: Enabling auto_push without reviewing commits first"
    With `auto_push = true`, every session close triggers a `git push` to the remote. If your vault has sensitive or draft content that should not leave your local machine, set `auto_push = false` until you have reviewed the staged content. Use `git log --oneline -5` after session close to inspect what was committed before enabling auto-push in production workflows.

!!! warning "Anti-Pattern: Setting min_score_threshold too low"
    Setting `min_score_threshold` below `0.4` will create noise — every pair of notes that shares one or two tokens in common will receive a link. Low-quality links reduce the signal-to-noise ratio in `graph related`, `graph rank`, and reweave-based session close enrichment. Start at `0.6` (default) and lower incrementally only if the graph is too sparse after a month of use.

!!! warning "Anti-Pattern: Disabling the git plugin after existing history"
    If you disable `[plugins.git] enabled = false` after already using the vault with git enabled, outstanding staged changes are left uncommitted forever. Future `ztlctl session close` calls will not commit them. If you want to stop tracking history mid-use, manually commit all staged files (`git commit -am "chore: disable git plugin"`) before setting `enabled = false`.

!!! warning "Anti-Pattern: Using [plugins.git] toml key for the Reweave config section"
    Reweave plugin config lives under `[reweave]` directly (not `[plugins.reweave]`). The Reweave plugin is a built-in whose config is managed by the top-level `ReweaveConfig` model, while the Git plugin uses the extensible `[plugins.git]` path under `PluginsConfig`. Mixing the two causes silent config fallback to defaults.

---

## Next Steps

- See [Agentic Workflows](agentic-workflows.md) for how plugins interact with sessions and MCP recipes
- See [Configuration](configuration.md) for the full ztlctl.toml reference including all plugin config schemas
- See [Best Practices](best-practices.md) for composing plugins with session-based workflows safely
- See [Obsidian Starter Kit](obsidian.md) for setting up the Obsidian integration


---

# Agentic Workflows

ztlctl is designed for agent-assisted capture and synthesis. Every command supports `--json`, sessions provide operational coordination, and topic packets provide read-oriented retrieval even when no session is active.

## Capture and Synthesis Workflow

Sessions are operational coordination state, not durable authored knowledge. Use them to structure research work, then turn findings into notes, references, and tasks:

```bash
# Agent starts a focused research session
ztlctl session start "API design patterns" --json

# Agent captures sources and synthesis
ztlctl ingest text "API source notes" --target-type reference --json
ztlctl create note "REST vs GraphQL trade-offs" \
  --tags architecture/api --json

# Agent logs its reasoning and costs
ztlctl session log "Analyzed 5 API frameworks" --cost 1200 --json
ztlctl session log "Key insight: GraphQL better for nested data" --pin --json

# Agent checks token budget
ztlctl session cost --report 50000 --json

# Agent requests context for continued work
ztlctl session context --topic "api" --budget 4000 --json

# Agent closes session, triggering capture/synthesis cleanup
ztlctl session close --summary "Mapped API paradigms" --json
```

## Ingestion

Core ingestion is text-first:

- raw text via `ztlctl ingest text`
- markdown and plain text files via `ztlctl ingest file`
- URLs via `ztlctl ingest url`, but only when a source-provider plugin is installed

```bash
ztlctl ingest text "OAuth notes" --target-type reference --json
ztlctl ingest file ./source.md --target-type note --json
ztlctl ingest providers --json
```

URL ingestion is provider-backed by design. The core tool does not ship a built-in remote fetcher in the base install.

For agent-fetched web and multimodal workflows, use a bundle-first handoff:

1. Fetch or extract the source outside ztlctl.
2. Normalize the capture into plain text plus a nested `source_bundle`.
3. Call MCP `ingest_source` with `content=<normalized text>` and `source_bundle=<bundle>`.
4. Let ztlctl persist the bundle beside the ingested reference under `sources/<reference-id>/`.

Read `ztlctl://capture/spec` for the exact bundle contract. The flat evidence-envelope fields (`source_kind`, `modalities`, `capture_agent`, `capture_method`, `citations`, `excerpts`, and `artifacts`) still work, but ztlctl now normalizes them into the durable source bundle format internally.

## Context Assembly (5-Layer System)

The `session context` command builds a token-budgeted payload with 5 layers:

| Layer | Content | Budget |
|-------|---------|--------|
| 0 — Identity | `self/identity.md` + `self/methodology.md` | Always included |
| 1 — Operational | Active session, recent decisions, work queue, log entries | Always included |
| 2 — Topic | Notes and references matching the session topic | Budget-dependent |
| 3 — Graph | 1-hop neighbors of Layer 2 content | Budget-dependent |
| 4 — Background | Recent activity, structural gaps | Budget-dependent |

The system tracks token usage per layer and reports pressure status (`normal`, `caution`, `exceeded`).

```bash
# Get full context with default 8000-token budget
ztlctl session context --json

# Focus on a topic with custom budget
ztlctl session context --topic "architecture" --budget 4000 --json

# Quick orientation (no session required)
ztlctl session brief --json
```

## Topic Packets

Use topic packets when you want conversational retrieval without depending on an active session:

```bash
ztlctl query packet architecture --mode learn --json
ztlctl query packet architecture --mode review --json
ztlctl query packet architecture --mode decision --json
```

Packets combine topic-matched notes, references, decisions, tasks, graph-adjacent material, evidence excerpts, supporting/conflicting links, stale items, bridge candidates, suggested actions, ranking explanations, and provenance maps so an agent can continue reasoning from captured knowledge rather than only from recent session state.

Packets merge topic-scoped items with search-ranked items, so a reference tagged to a topic is still available for review and learning even when its title/body is a weak lexical match for the topic string itself.

When a packet should become durable work, draft from it directly:

```bash
ztlctl query draft architecture --target note --json
ztlctl query draft architecture --mode review --target task --json
ztlctl query draft architecture --mode decision --target decision --json
```

For a portable human review surface outside the vault, use `ztlctl export dashboard ./output/ --viewer obsidian`. That export is an external review workbench for machine-layer queues, stale/orphan signals, and topic dossiers; it complements `garden/` but does not write into the vault or mirror `.obsidian/` state.

## Session Close Enrichment Pipeline

When a session closes, ztlctl automatically runs:

1. **Cross-session reweave** — discovers connections for all notes created in the session
2. **Orphan sweep** — attempts to connect orphan notes (0 outgoing edges)
3. **Integrity check** — validates vault consistency
4. **Graph materialization** — updates PageRank, degree, and betweenness metrics

Each step can be toggled in `ztlctl.toml`:

```toml
[session]
close_reweave = true
close_orphan_sweep = true
close_integrity_check = true
```

## Decision Extraction

Extract decisions from session logs into permanent decision notes:

```bash
# Extracts pinned/decision entries from the session log
ztlctl session extract LOG-0001 --title "Decision: Use GraphQL for nested queries"
```

This creates a decision note (`subtype=decision`, status `proposed`) linked to the session via a `derived_from` edge.

## MCP Server Integration

ztlctl includes a Model Context Protocol (MCP) server for direct integration with AI clients like Claude Desktop and Codex-compatible environments:

```bash
ztlctl serve --transport stdio
```

Use the discovery flow in MCP clients:

1. `discover_categories`
2. `activate_category` (for non-core categories)
3. `ztlctl://agent-reference`

For enrichment-focused agents, the most useful read resources are:

- `ztlctl://review/dashboard`
- `ztlctl://garden/backlog`
- `ztlctl://decision-queue`
- `ztlctl://capture/spec`

The MCP prompt layer includes `topic_learn`, `topic_review`, `topic_decision`, `capture_web_source`, and `capture_multimodal_source`.

See the [MCP Server](mcp.md) page for tool categories, resources, prompts, and exported client assets.

## Recipe Walkthroughs

ztlctl exposes three structured workflow recipes via MCP. Each recipe is a sequence of tool calls that accomplishes a common knowledge-work task. Recipes are designed for agent-driven execution but every step has a human CLI equivalent.

Access recipes via MCP resource discovery:

```bash
# Discover available recipes
ztlctl://recipes
```

Or access individual recipe specs directly in your MCP client:

- `ztlctl://recipes/research-capture`
- `ztlctl://recipes/review-triage`
- `ztlctl://recipes/knowledge-synthesis`

### Recipe 1: Research Capture

**What it accomplishes:** Search existing knowledge for a topic, create a synthesis note for new findings, and automatically connect it to related content via reweave.

**When to use:** When an agent (or you) has gathered research on a topic and wants to turn it into a durable knowledge artifact linked to existing context.

**Steps:**

| Step | Action | Condition |
|------|--------|-----------|
| 1 | Search existing content for the topic (limit 10) | Always |
| 2 | Create a seed note with the synthesis title | Skip if step 1 already has a note with the same title |
| 3 | Reweave the new note against existing content | Always |

**Human CLI walkthrough:**

```bash
# Step 1: Check if you already have related content
ztlctl query search "oauth security" --limit 10

# Expected output (if no related notes yet):
# No results for "oauth security"

# Step 2: Create a synthesis note
ztlctl create note "OAuth Security Patterns" --tags auth/oauth --tags security

# Expected output:
# Created note ZTL-0042: OAuth Security Patterns [seed]
# Reweave: 3 links created (auto-triggered by Reweave plugin)

# Step 3: Manual reweave if needed (already ran automatically above)
# ztlctl reweave run --content-id ZTL-0042
```

!!! tip
    The Reweave plugin runs automatically after `create note`, so step 3 is already done for you. Run `ztlctl reweave run --content-id {id}` manually only if you disabled auto-reweave or want to re-run after adding more related content. See [Built-in Plugins](plugins.md) for Reweave configuration.

**Agent MCP tool sequence:**

```
1. search(query="oauth security", limit=10)
2. create_note(title="OAuth Security Patterns", tags=["auth/oauth", "security"])
3. reweave(content_id=<step_2_id>)
```

### Recipe 2: Review Triage

**What it accomplishes:** Surface the current work queue, inspect each actionable item, update stale notes, and archive completed or obsolete ones.

**When to use:** Periodic review — clearing the backlog, promoting mature notes, archiving abandoned tasks.

**Steps:**

| Step | Action | Condition |
|------|--------|-----------|
| 1 | Get the full work queue | Always |
| 2 | Fetch each item's full content | Repeat for each work queue item |
| 3 | Update items that need changes | Skip if item is already current |
| 4 | Archive items that are complete or irrecoverable | Only if item should not continue |

**Human CLI walkthrough:**

```bash
# Step 1: Get the prioritized work queue
ztlctl query work-queue

# Expected output (example):
# TASK-0001  "Follow up on OAuth threat model"   priority: high   maturity: seed   stale: 14 days
# TASK-0002  "Review Attention paper"             priority: medium maturity: seed   stale: 7 days
# ZTL-0035   "Token exchange trade-offs"          maturity: seed   no outgoing edges

# Step 2: Inspect a specific item
ztlctl query get TASK-0001

# Step 3: Update an item (promote maturity, add notes)
ztlctl update TASK-0001 --maturity budding

# Expected output:
# Updated TASK-0001: maturity seed -> budding

# Step 4: Archive a completed item
ztlctl archive TASK-0001

# Expected output:
# Archived TASK-0001: "Follow up on OAuth threat model"
```

**Agent MCP tool sequence:**

```
1. work_queue()
2. get(content_id=<each_item_id>)  <- repeat per item
3. update(content_id=<id>, changes={maturity: "budding"})  <- skip if no changes needed
4. archive(content_id=<id>)  <- only if item is done or stale beyond recovery
```

### Recipe 3: Knowledge Synthesis

**What it accomplishes:** Search a topic broadly, identify structural gaps in the knowledge graph, draft a synthesis note from the topic packet, and reweave it into the graph.

**When to use:** When you want to consolidate scattered knowledge on a topic into a single synthesis artifact that explicitly acknowledges gaps and invites future connection.

**Steps:**

| Step | Action | Condition |
|------|--------|-----------|
| 1 | Search existing content (limit 20) | Always |
| 2 | Find graph gaps — structurally isolated areas | Always |
| 3 | Draft a synthesis note from the topic packet | Skip if step 1 already has a mature (evergreen) synthesis note |
| 4 | Reweave the draft against existing content | Always |

**Human CLI walkthrough:**

```bash
# Step 1: Survey what exists on the topic
ztlctl query search "distributed systems" --limit 20

# Expected output (example):
# ZTL-0010  "CAP theorem trade-offs"        maturity: budding
# ZTL-0018  "Raft consensus algorithm"      maturity: seed
# REF-0003  "Designing Data-Intensive Apps"  maturity: evergreen

# Step 2: Find gaps — where the graph is thin
ztlctl graph gaps --top 10

# Expected output (example):
# Gap 1: "consensus" cluster — 3 notes, 1 bridge connection, no synthesis note
# Gap 2: "replication" cluster — isolated from "consistency" cluster

# Step 3: Draft a synthesis note from the topic
ztlctl query draft "distributed-systems" --target note

# Expected output:
# Drafted ZTL-0055: "Distributed Systems Synthesis" [seed]
#   Sources: 8 notes, 2 references
#   Gaps surfaced: consensus <-> replication bridge missing

# Step 4: Reweave (already ran automatically — manual run if re-running)
ztlctl reweave run --content-id ZTL-0055
```

**Agent MCP tool sequence:**

```
1. search(query="distributed systems", limit=20)
2. gaps(top=10)
3. draft_from_topic(topic="distributed-systems", target="note")  <- skip if step 1 has mature synthesis
4. reweave(content_id=<step_3_id>)
```

!!! tip
    Recipes are starting points, not rigid scripts. An agent can modify steps based on what it finds — for example, skipping the draft step if a recent evergreen note already covers the topic.

## Session Lifecycle

Sessions are operational coordination units — they group a period of work so ztlctl can apply enrichment to everything created during that period when the session closes. Sessions are optional for human users and central to agent-driven workflows.

### Human-Driven Session

A typical 30-minute research session:

```bash
# Start a session with a topic focus
ztlctl session start "oauth security research"

# Expected output:
# Session started: LOG-0001 "oauth security research"
# Session is open. All notes and references created now are linked to LOG-0001.

# Capture sources as you find them
ztlctl ingest text "RFC 6749 key sections" --target-type reference --tags auth/oauth
ztlctl create note "Token exchange threat model" --tags auth/oauth --tags security --topic auth

# Log your reasoning as you go
ztlctl session log "Read RFC 6749 sections 4-6. Key risk: token replay via HTTP."
ztlctl session log "Drafted threat model. Needs peer review." --pin

# Check how much context you have accumulated (useful before asking an agent to continue)
ztlctl session cost --report 50000

# Close the session when done
ztlctl session close --summary "Mapped OAuth 2.0 attack surface"

# Expected output:
# Session closed: LOG-0001
# Cross-session reweave: 7 new links created
# Orphan sweep: 2 orphaned notes connected
# Integrity check: 0 issues
# Graph metrics updated (PageRank, degree, betweenness)
```

!!! note
    Only one session can be open at a time. If you already have an open session, `ztlctl session start` returns an error.

### Agent-Driven Session

Agents use the same session commands via MCP tools. A literature review agent might:

```
# Agent starts session
start(topic="attention mechanisms in transformers")

# Agent fetches topic context to understand what is already captured
context(topic="transformers", budget=8000)

# Agent ingests retrieved sources
ingest_source(title="Attention Is All You Need", content=<paper_text>, input_kind="text", target_type="reference")

# Agent creates synthesis notes as it reads
create_note(title="Multi-head attention intuition", tags=["ml/transformers"])
create_note(title="Self-attention vs cross-attention", tags=["ml/transformers"])

# Agent logs key decisions
log_entry(message="Core insight: Q/K/V projection sizes determine capacity vs. compute tradeoff", pin=True)

# Agent closes session when done
close(summary="Surveyed attention mechanism architecture, 2 synthesis notes created")
```

The agent receives structured JSON at each step, including content IDs it can use in subsequent calls. No special agent-mode configuration is required — the same MCP tools work for both supervised and autonomous agents.

### Session Close Enrichment Pipeline

When you (or an agent) call `session close`, ztlctl runs a 5-step enrichment pipeline automatically:

```
LOG CLOSE -> CROSS-SESSION REWEAVE -> ORPHAN SWEEP -> INTEGRITY CHECK -> GRAPH MATERIALIZATION
```

**Step 1 — Log Close:** The session node is marked `status="closed"` and a `session_close` log entry is inserted. This is atomic — if it fails, no other steps run.

**Step 2 — Cross-Session Reweave** (toggle: `close_reweave`): Runs the reweave algorithm on every note and reference created during this session. New connections are discovered across the full vault — not just within the session. Returns the count of new links created.

**Step 3 — Orphan Sweep** (toggle: `close_orphan_sweep`): Finds every note and reference in the entire vault with zero outgoing edges — not just session notes. Runs reweave with a lower threshold (default: 0.2 instead of 0.6) so orphaned notes have a better chance of getting at least one connection.

**Step 4 — Integrity Check** (toggle: `close_integrity_check`): Runs the vault integrity checker. Counts error-severity issues and appends a warning to the close result. Does **not** auto-fix — only reports.

**Step 5 — Graph Materialization** (always runs): Updates PageRank, degree centrality, and betweenness centrality for all nodes in the graph. This powers rank-ordered search, bridge detection, and gap identification.

**Reading the close result with `--json`:**

```bash
ztlctl session close --summary "Research complete" --json
```

```json
{
  "session_id": "LOG-0001",
  "status": "closed",
  "reweave_count": 7,
  "orphan_count": 2,
  "integrity_issues": 0
}
```

- `reweave_count`: Total new graph edges created across all session notes
- `orphan_count`: Number of previously-isolated notes that received at least one connection via the orphan sweep
- `integrity_issues`: Count of error-severity integrity violations found (0 = healthy)

**Configuring the pipeline** in `ztlctl.toml`:

```toml
[session]
close_reweave = true
close_orphan_sweep = true
close_integrity_check = true
orphan_reweave_threshold = 0.2  # lower = more connections for orphans
```

## Batch Operations

For programmatic creation, use batch mode with a JSON file:

```bash
echo '[
  {"type": "note", "title": "Concept A", "tags": ["domain/scope"]},
  {"type": "reference", "title": "Source B", "url": "https://example.com"},
  {"type": "task", "title": "Follow up on C", "priority": "high"}
]' > items.json

ztlctl create batch items.json --json
ztlctl create batch items.json --partial  # Continue on individual failures
```

## Scripting with JSON Output

Every command supports `--json` for structured output:

```bash
# Create and capture the ID
ID=$(ztlctl create note "My Note" --json | jq -r '.data.id')

# Query and process results
ztlctl query search "python" --json | jq '.data.items[].title'

# Check vault health programmatically
ERRORS=$(ztlctl check check --json | jq '.data.issues | map(select(.severity == "error")) | length')
```

## Anti-Patterns

!!! warning "Anti-Pattern: Agent creating notes without an active session"
    Notes created outside a session are not linked to a coordination unit, so the session-close enrichment pipeline (cross-session reweave, orphan sweep, integrity check) never runs for them. Individual Reweave plugin auto-reweave still fires, but session-level enrichment does not. For agent-driven workflows, always call `start` before creating notes and `close` when done.

!!! warning "Anti-Pattern: Agent ignoring ServiceResult.success"
    Every MCP tool returns a `ServiceResult` with an `ok` boolean. Agents that skip this check and proceed on failure will chain bad state — creating notes with IDs that do not exist, updating records that were never written, or extracting decisions from sessions that failed to close. Always gate the next step on `result.ok == true` and surface the `error.message` field when false.

!!! warning "Anti-Pattern: Agent running reweave run after every single create"
    The Reweave plugin already runs `reweave run` automatically after every `create_note` and `create_reference` call. Calling `reweave` manually after each individual create causes double-reweave on the same item, wasting time and producing duplicate-edge attempts. Use `session close` to trigger a single cross-session reweave on all notes created during a session. Reserve manual `reweave run --content-id {id}` calls for notes with `--no-reweave` or notes created before a new related item was added to the vault.

!!! warning "Anti-Pattern: Agent using raw SQL instead of CLI/MCP tools"
    The SQLite database at `.ztlctl/ztlctl.db` is managed exclusively by ztlctl. Agents that query or mutate it directly bypass frontmatter sync, tag indexing, graph edge maintenance, FTS5 updates, and the event bus. The result is a database that drifts out of sync with the filesystem. All knowledge-work operations must go through the CLI (`ztlctl query search`, `ztlctl create note`) or MCP tools (`search`, `create_note`). Use `ztlctl check rebuild` to recover if the database has already been corrupted by direct SQL access.

---

## Next Steps

- See [Built-in Plugins](plugins.md) for Git and Reweave plugin configuration
- See [Best Practices](best-practices.md) for composing sessions, reweave, and agents safely
- See [Agents Reference](agents.md) for MCP tool schemas, session state machine, and error recovery
- See [Knowledge Paradigms](paradigms.md) for second-brain vs knowledge garden approach guidance
- See [Core Concepts](concepts.md) for the content type and maturity tier reference
- See [MCP Server](mcp.md) for the full MCP tool and resource reference


---

# Command Reference

Every command example on this page is verified against the Click command source in `src/ztlctl/commands/` and the ActionDefinition registry in `src/ztlctl/actions/_register_core.py`. See [Configuration](configuration.md) for config-based defaults.

## Global Options

Every command supports these flags:

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--version` | flag | — | Show version and exit |
| `--json` | flag | false | Structured JSON output (for scripting and agents) |
| `-q, --quiet` | flag | false | Minimal output |
| `-v, --verbose` | flag | false | Detailed output with span tree and debug info |
| `--log-json` | flag | false | Structured JSON log output to stderr (structlog) |
| `--no-interact` | flag | false | Non-interactive mode (no prompts) |
| `--no-reweave` | flag | false | Skip automatic reweave on creation |
| `-c, --config TEXT` | text | — | Override config file path |
| `--sync` | flag | false | Force synchronous event dispatch |

Most commands also support `--examples` to show usage examples.

## Commands at a Glance

| Command | Purpose |
|---------|---------|
| `init [OPTIONS]` | Initialize a new vault (interactive wizard) |
| `init regenerate` | Re-render self/ agent context files |
| `init staleness` | Check whether self/ files need regeneration |
| `create note TITLE` | Create a note |
| `create reference TITLE` | Create a reference |
| `create task TITLE` | Create a task |
| `create batch FILE` | Batch create from JSON |
| `query search QUERY` | Full-text search |
| `query get ID` | Get item by ID |
| `query list` | List with filters |
| `query count` | Total indexed item count |
| `query tags` | List active tags with usage counts |
| `query work-queue` | Prioritized task queue |
| `query decision-support` | Decision context aggregation |
| `query packet TOPIC` | Topic-scoped learning/review packet |
| `query draft TOPIC` | Draft a note, task, or decision from a topic packet |
| `query review` | Vault health and structure snapshot |
| `graph related ID` | Find related content |
| `graph themes` | Discover topic clusters |
| `graph rank` | PageRank importance |
| `graph path SRC DST` | Shortest path between nodes |
| `graph gaps` | Find structural holes |
| `graph bridges` | Find bridge nodes |
| `graph unlink SRC DST` | Remove link between two nodes |
| `graph materialize` | Compute and store graph metrics |
| `session start TOPIC` | Start a session |
| `session close` | Close with enrichment pipeline |
| `session reopen ID` | Reopen a closed session |
| `session status` | Show active session summary |
| `session cost` | Token cost tracking |
| `session log MSG` | Append log entry |
| `session context` | Token-budgeted context payload |
| `session brief` | Quick orientation summary |
| `session extract SESSION_ID` | Extract decision from session |
| `check check` | Report integrity issues |
| `check fix` | Auto-repair integrity issues |
| `check rebuild` | Full DB rebuild from filesystem |
| `check rollback` | Restore DB from latest backup |
| `update ID` | Update metadata or body |
| `reweave run` | Run link discovery on vault or a specific item |
| `reweave prune` | Remove stale links below threshold |
| `reweave undo` | Reverse a reweave operation |
| `archive ID` | Soft-delete content |
| `supersede OLD NEW` | Mark decision as superseded |
| `export markdown OUTPUT_DIR` | Export as portable markdown |
| `export indexes OUTPUT_DIR` | Generate type/topic indexes |
| `export graph` | Export graph (DOT or JSON) |
| `export dashboard OUTPUT_DIR` | Export an external review workbench |
| `garden seed TITLE` | Quick-capture seed note |
| `ingest text TITLE` | Ingest raw text into a note or reference |
| `ingest file PATH` | Ingest a markdown or text file |
| `ingest url URL` | Ingest a URL through an installed source provider |
| `ingest providers` | List installed source providers |
| `docs search QUERY` | Search ztlctl documentation corpus |
| `serve` | Start MCP server |
| `upgrade check` | List pending migrations |
| `upgrade apply` | Apply pending migrations |
| `upgrade stamp` | Stamp DB as current head |
| `vector status` | Check semantic search availability |
| `vector reindex` | Rebuild the vector index |
| `workflow init VAULT_ROOT` | Initialize workflow scaffolding |
| `workflow update VAULT_ROOT` | Update workflow scaffolding |
| `workflow export VAULT_ROOT` | Export agent workflow assets |

## Command Details

### init

```bash
ztlctl init [--path PATH] [--name NAME] [--profile PROFILE] [--tone TONE] [--topics TEXT] [--no-workflow]
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--path TEXT` | text | `.` | Vault directory |
| `--name TEXT` | text | — | Vault display name (prompted if omitted in interactive mode) |
| `--profile TEXT` | text | `core` | Workspace profile (`core`, `obsidian`, or plugin-defined) |
| `--tone TEXT` | choice | `research-partner` | Agent tone: `research-partner`, `assistant`, or `minimal` |
| `--topics TEXT` | text | — | Comma-separated topic directories |
| `--no-workflow` | flag | false | Skip workflow template setup |

### create note

```bash
ztlctl create note TITLE [--subtype TEXT] [--tags TEXT]... [--topic TEXT] [--body TEXT] [--key-points TEXT]... [--links JSON] [--aliases TEXT]...
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--subtype TEXT` | text | — | Optional subtype (e.g. `knowledge`, `decision`, or plugin-defined) |
| `--tags TEXT` | multiple | — | Tags (repeatable: `--tags ml/ai --tags research/papers`) |
| `--topic TEXT` | text | — | Topic directory under notes/ |
| `--body TEXT` | text | — | Markdown body |
| `--key-points TEXT` | multiple | — | Bullet-style summary items (repeatable) |
| `--links JSON` | json | — | Explicit edge map keyed by relation type |
| `--aliases TEXT` | multiple | — | Alternate names for search and linking (repeatable) |

### create reference

```bash
ztlctl create reference TITLE [--url TEXT] [--subtype TEXT] [--tags TEXT]... [--topic TEXT] [--body TEXT] [--summary TEXT]
```

### create task

```bash
ztlctl create task TITLE [--priority CHOICE] [--impact CHOICE] [--effort CHOICE] [--tags TEXT]...
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--priority` | choice | `medium` | `low`, `medium`, or `high` |
| `--impact` | choice | `medium` | `low`, `medium`, or `high` |
| `--effort` | choice | `medium` | `low`, `medium`, or `high` |
| `--tags TEXT` | multiple | — | Tags (repeatable) |

### create batch

```bash
ztlctl create batch FILE [--partial]
```

FILE must be a JSON array of objects, each with `type` and at minimum `title` keys.

### query search

```bash
ztlctl query search QUERY [--type CHOICE] [--tag TEXT] [--space CHOICE] [--rank-by CHOICE] [--limit INT]
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--type` | choice | — | `note`, `reference`, `task`, or `log` |
| `--tag TEXT` | text | — | Tag filter |
| `--space` | choice | — | `notes`, `ops`, or `self` |
| `--rank-by` | choice | `relevance` | See [Search Ranking Modes](#search-ranking-modes) |
| `--limit INT` | int | `20` | Maximum results |

### query list

```bash
ztlctl query list [--type CHOICE] [--status TEXT] [--tag TEXT] [--topic TEXT] [--subtype TEXT] [--maturity CHOICE] [--space CHOICE] [--since TEXT] [--include-archived] [--sort CHOICE] [--limit INT]
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--type` | choice | — | `note`, `reference`, `task`, or `log` |
| `--status TEXT` | text | — | Status filter |
| `--tag TEXT` | text | — | Tag filter |
| `--topic TEXT` | text | — | Topic filter |
| `--subtype TEXT` | text | — | Subtype filter |
| `--maturity` | choice | — | `seed`, `sprout`, or `evergreen` |
| `--space` | choice | — | `notes`, `ops`, or `self` |
| `--since TEXT` | text | — | Lower bound date (e.g. `2025-01-01`) |
| `--include-archived` | flag | false | Include archived items |
| `--sort` | choice | `recency` | `recency`, `title`, `type`, or `priority` |
| `--limit INT` | int | `20` | Maximum items |

### update

```bash
ztlctl update CONTENT_ID [--title TEXT] [--status TEXT] [--tags TEXT]... [--topic TEXT] [--body TEXT] [--maturity CHOICE]
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--title TEXT` | text | — | New title |
| `--status TEXT` | text | — | New status (must be a valid lifecycle transition) |
| `--tags TEXT` | multiple | — | Replace tags (repeatable) |
| `--topic TEXT` | text | — | New topic subdirectory |
| `--body TEXT` | text | — | New body text |
| `--maturity` | choice | — | `seed`, `budding`, or `evergreen` |

### reweave

```bash
ztlctl reweave run [--content-id TEXT] [--dry-run] [--min-score-override FLOAT]
ztlctl reweave prune [--content-id TEXT] [--dry-run]
ztlctl reweave undo [--reweave-id INT]
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--content-id TEXT` | text | — | Target item ID (omit to run across the vault) |
| `--dry-run` | flag | false | Preview without writing changes |
| `--min-score-override FLOAT` | float | — | Override minimum relevance threshold |

### check

```bash
ztlctl check check [--min-severity CHOICE]
ztlctl check fix [--level CHOICE]
ztlctl check rebuild
ztlctl check rollback
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--min-severity` | choice | `warning` | `info`, `warning`, or `error` |
| `--level` | choice | `safe` | `safe` or `aggressive` |

### session

```bash
ztlctl session start TOPIC
ztlctl session close [--summary TEXT]
ztlctl session reopen SESSION_ID
ztlctl session status
ztlctl session log MESSAGE [--pin] [--cost INT] [--detail TEXT]
ztlctl session cost [--report INT]
ztlctl session context [--topic TEXT] [--budget INT] [--ignore-checkpoints]
ztlctl session brief
ztlctl session extract SESSION_ID [--title TEXT]
```

### graph

```bash
ztlctl graph related CONTENT_ID [--depth INT] [--top INT]
ztlctl graph themes
ztlctl graph rank [--top INT]
ztlctl graph path SOURCE_ID TARGET_ID
ztlctl graph gaps [--top INT]
ztlctl graph bridges [--top INT]
ztlctl graph unlink SOURCE_ID TARGET_ID [--both]
ztlctl graph materialize
```

### export

```bash
ztlctl export markdown OUTPUT_DIR [--type CHOICE]
ztlctl export indexes OUTPUT_DIR [--type CHOICE]
ztlctl export graph [--format CHOICE] [--output FILE] [--type CHOICE]
ztlctl export dashboard OUTPUT_DIR [--viewer TEXT]
```

### docs

```bash
ztlctl docs search QUERY [--limit INT] [--json]
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--limit INT` | int | `5` | Maximum number of results |
| `--json` | flag | false | Output results as JSON |

The `docs search` command searches the bundled ztlctl documentation corpus using TF-IDF and returns page titles, scores, and excerpts. This is also available as the `docs_search` MCP tool for agent-driven documentation lookup.

### ingest

```bash
ztlctl ingest text TITLE [--target-type CHOICE] [--topic TEXT] [--tags TEXT]... [--summary TEXT] [--dry-run]
ztlctl ingest file PATH [--title TEXT] [--target-type CHOICE] [--topic TEXT] [--tags TEXT]... [--summary TEXT] [--dry-run]
ztlctl ingest url URL [--provider TEXT] [--title TEXT] [--target-type CHOICE] [--topic TEXT] [--tags TEXT]... [--summary TEXT] [--dry-run]
ztlctl ingest providers
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--target-type` | choice | — | `reference` or `note` |
| `--tags TEXT` | multiple | — | Tags (repeatable) |
| `--dry-run` | flag | false | Preview without writing |

## Search Ranking Modes

The `--rank-by` option on `query search` supports seven modes:

| Mode | Algorithm | Best For |
|------|-----------|----------|
| `relevance` | BM25 | General lexical search |
| `recency` | BM25 with time decay | Recent-but-relevant work |
| `graph` | BM25 x PageRank boost | Finding well-connected content |
| `semantic` | Vector similarity | Meaning-based retrieval |
| `hybrid` | BM25 + vector merge | Mixed lexical and semantic discovery |
| `review` | Enrichment rerank over search results | Stale, weakly connected, review-worthy items |
| `garden` | Enrichment rerank with provenance/maturity signals | Human-led enrichment and garden work |

Search results in `--json` mode include a `ranking` block with reasons and signal scores so agents can explain why an item surfaced.

## Query Filters

The `query list` command supports composable filters:

```bash
# Combine any of these:
ztlctl query list --type note|reference|task|log
ztlctl query list --status draft|linked|connected|inbox|active|done
ztlctl query list --tag "domain/scope"
ztlctl query list --topic "python"
ztlctl query list --subtype knowledge|decision|article|tool|spec
ztlctl query list --maturity seed|sprout|evergreen
ztlctl query list --space notes|ops|self
ztlctl query list --since 2025-01-01
ztlctl query list --include-archived
ztlctl query list --sort recency|title|type|priority
ztlctl query list --limit 50
```

## Ingestion

Use ingestion when the source material should be normalized into a durable artifact:

```bash
ztlctl ingest text "OAuth Notes" --target-type reference
ztlctl ingest file ./source.md --target-type note
ztlctl ingest providers
ztlctl ingest url https://example.com/spec --provider my-provider --target-type reference
```

URL ingestion is provider-backed. If no source provider is installed, `ingest url` returns `NO_PROVIDER` and suggests `ztlctl ingest providers`.

For richer agent-driven capture, the MCP `ingest_source` tool accepts a nested `source_bundle` object. ztlctl persists that bundle beside the created reference under `sources/<reference-id>/bundle.json` plus `sources/<reference-id>/normalized.md`.

Read `ztlctl://capture/spec` for the exact bundle contract.

## Topic Packets and Drafts

Use packets when you want a conversational bundle rather than a flat result list:

```bash
ztlctl query packet architecture --mode learn
ztlctl query packet architecture --mode review
ztlctl query packet architecture --mode decision
```

Packets include evidence excerpts, supporting/conflicting links, stale items, bridge candidates, suggested actions, and ranking explanations.

Use drafts when you want the next durable artifact sketched from the packet:

```bash
ztlctl query draft architecture --target note
ztlctl query draft architecture --mode review --target task
ztlctl query draft architecture --mode decision --target decision
```


---

# Configuration

ztlctl uses a `ztlctl.toml` file at the vault root. This file uses a sparse TOML contract — only include the settings you want to override. All defaults are baked into the source, so a fresh vault needs only `[vault] name` and `[agent] tone` to be functional.

Settings can also be overridden via `ZTLCTL_*` environment variables (see [Environment Variables](#environment-variables)).

## File Location

`ztlctl.toml` lives at the vault root — the directory returned by `ztlctl vault path`. Every ztlctl command walks up from the current directory until it finds this file.

```
my-vault/
  ztlctl.toml        ← configuration lives here
  .ztlctl/           ← database and backups (managed by ztlctl)
  self/              ← agent-facing vault identity
  notes/
  ops/
```

## Config Sections

### [vault]

Vault identity and workspace client.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | string | `"my-vault"` | Vault display name used in exports and agent context |
| `client` | string | `"none"` | Deprecated compatibility field. Use `[workspace] profile` instead. |

```toml
[vault]
name = "research-vault"
```

> **Note**: `[vault].client` is a deprecated compatibility input. Legacy values `none` and `vanilla` map to `profile = "core"` during settings load.

---

### [workspace]

Workspace profile selection — controls which scaffold, templates, and integrations are active.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `profile` | string | `"core"` | Workspace profile. `core` is always available; additional profiles come from installed plugins. |

```toml
[workspace]
profile = "core"         # or "obsidian" for Obsidian starter kit
```

> **Note**: `workspace.profile = "obsidian"` enables the first-party Obsidian starter kit during `ztlctl init`. That scaffold writes `.obsidian/` config, `garden/` templates, and a plugin-install checklist, but it does not download Obsidian community plugins.

---

### [agent]

Controls how ztlctl communicates with and assembles context for AI agents.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `tone` | string | `"research-partner"` | Agent communication style. Options: `"research-partner"`, `"assistant"`, `"minimal"` |

```toml
[agent]
tone = "research-partner"
```

---

### [agent.context]

Token budget and layer sizing for the `ztlctl agent context` command.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_budget` | int | `8000` | Token budget for context assembly |
| `layer_0_min` | int | `500` | Minimum tokens reserved for Layer 0 (vault identity + current note) |
| `layer_1_min` | int | `1000` | Minimum tokens reserved for Layer 1 (direct links) |
| `layer_2_max_notes` | int | `10` | Maximum notes included in Layer 2 (topic cluster) |
| `layer_3_max_hops` | int | `1` | Graph traversal depth for Layer 3 (neighborhood) |

```toml
[agent.context]
default_budget = 16000   # Increase for larger context windows
layer_0_min = 500
layer_1_min = 1000
layer_2_max_notes = 10
layer_3_max_hops = 1
```

---

### [reweave]

Controls the automatic link discovery engine that runs after note creation and on session close.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Enable or disable automatic link discovery |
| `min_score_threshold` | float | `0.6` | Minimum composite score to suggest a link (0.0–1.0) |
| `max_links_per_note` | int | `5` | Maximum outbound links reweave will add per note |
| `lexical_weight` | float | `0.35` | Weight for BM25 lexical similarity signal |
| `tag_weight` | float | `0.25` | Weight for Jaccard tag overlap signal |
| `graph_weight` | float | `0.25` | Weight for graph proximity signal |
| `topic_weight` | float | `0.15` | Weight for topic cluster membership signal |

All four weights should sum to 1.0.

```toml
[reweave]
enabled = true
min_score_threshold = 0.6
max_links_per_note = 5
lexical_weight = 0.35
tag_weight = 0.25
graph_weight = 0.25
topic_weight = 0.15
```

---

### [garden]

Thresholds for the knowledge garden health system. These values determine when notes are flagged as stale seeds or qualify for evergreen status.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `seed_age_warning_days` | int | `7` | Days before a seed note is flagged as stale |
| `evergreen_min_key_points` | int | `5` | Minimum key points required for evergreen qualification |
| `evergreen_min_bidirectional_links` | int | `3` | Minimum bidirectional links required for evergreen qualification |

```toml
[garden]
seed_age_warning_days = 7
evergreen_min_key_points = 5
evergreen_min_bidirectional_links = 3
```

---

### [search]

Hybrid search configuration controlling both lexical (BM25) and semantic (vector) retrieval.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `semantic_enabled` | bool | `false` | Enable vector-based semantic search (requires `ztlctl[semantic]` extra) |
| `embedding_model` | string | `"local"` | Embedding model to use. `"local"` uses bundled sentence-transformers model |
| `embedding_dim` | int | `384` | Embedding vector dimension — must match the chosen model |
| `half_life_days` | float | `30.0` | Time-decay half-life for recency boosting in search ranking |
| `semantic_weight` | float | `0.5` | Blend weight for semantic vs. lexical scoring in hybrid mode |

```toml
[search]
semantic_enabled = false
embedding_model = "local"
embedding_dim = 384
half_life_days = 30.0
semantic_weight = 0.5
```

To enable semantic search: `pip install ztlctl[semantic]` then set `semantic_enabled = true`.

---

### [ingest]

Controls the URL and file ingestion pipeline.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Enable or disable the ingestion pipeline |
| `auto_reweave` | bool | `true` | Automatically reweave after each ingested note |
| `default_target_type` | string | `"reference"` | Default note type for ingested content |
| `providers` | table | `{}` | Per-provider configuration injected by source-provider plugins |

```toml
[ingest]
enabled = true
auto_reweave = true
default_target_type = "reference"

[ingest.providers]
# Provider-specific overrides live here when installed plugins support them
```

---

### [session]

Controls what happens when you run `ztlctl session close`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `close_reweave` | bool | `true` | Run reweave on session close |
| `close_orphan_sweep` | bool | `true` | Connect orphan notes on session close |
| `close_integrity_check` | bool | `true` | Run integrity check on session close |
| `orphan_reweave_threshold` | float | `0.2` | Minimum score for orphan sweep connections (lower = more aggressive) |

```toml
[session]
close_reweave = true
close_orphan_sweep = true
close_integrity_check = true
orphan_reweave_threshold = 0.2
```

---

### [tags]

Controls automatic tag management.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `auto_register` | bool | `true` | Automatically register new tags in the tag index when notes are created |

```toml
[tags]
auto_register = true
```

---

### [check]

Controls backup retention for the vault integrity system.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `backup_retention_days` | int | `30` | Days to retain backups before pruning |
| `backup_max_count` | int | `10` | Maximum number of backups to keep regardless of age |

```toml
[check]
backup_retention_days = 30
backup_max_count = 10
```

---

### [plugins.git]

Configuration for the built-in Git plugin. This is a `[plugins.<name>]` sub-table — not a top-level `[git]` section.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Enable or disable the Git plugin |
| `batch_commits` | bool | `true` | Batch commits to session close instead of committing per-operation |
| `auto_push` | bool | `false` | Automatically push after commit |
| `auto_ignore` | bool | `true` | Automatically add ztlctl-managed paths to `.gitignore` |
| `branch` | string | `"develop"` | Default branch for git operations |
| `commit_style` | string | `"conventional"` | Commit message format. Options: `"conventional"`, `"simple"` |

```toml
[plugins.git]
enabled = true
batch_commits = true
auto_push = false
auto_ignore = true
branch = "develop"
commit_style = "conventional"
```

See [Built-in Plugins](plugins.md) for full Git plugin documentation.

---

### [mcp]

Controls the MCP server launched by `ztlctl serve`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Enable or disable MCP server functionality |
| `transport` | string | `"stdio"` | MCP transport. `"stdio"` for Claude Desktop; `"http"` for remote clients |

```toml
[mcp]
enabled = true
transport = "stdio"
```

---

### [workflow]

Controls Copier-based workflow template scaffolding (used with `ztlctl workflow init`).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `template` | string | `"claude-driven"` | Workflow template name or path |
| `skill_set` | string | `"research"` | Skill set profile injected into the generated workflow |

```toml
[workflow]
template = "claude-driven"
skill_set = "research"
```

---

### [exports.dashboard]

Controls what is included in the agent dashboard export generated by `ztlctl export`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `include_work_queue` | bool | `true` | Include the work queue in dashboard output |
| `include_recent_decisions` | bool | `true` | Include recent decision notes |
| `include_garden_backlog` | bool | `true` | Include garden health backlog |
| `topic_dossier_limit` | int | `5` | Maximum number of topic dossiers to include |

```toml
[exports.dashboard]
include_work_queue = true
include_recent_decisions = true
include_garden_backlog = true
topic_dossier_limit = 5
```

---

## Environment Variables

### Standard Override Pattern

Any `ztlctl.toml` setting can be overridden with a `ZTLCTL_` prefix. Nested keys use double underscores (`__`) as separators:

```bash
ZTLCTL_REWEAVE__MIN_SCORE_THRESHOLD=0.4 ztlctl reweave
ZTLCTL_AGENT__CONTEXT__DEFAULT_BUDGET=16000 ztlctl agent context
ZTLCTL_SESSION__CLOSE_REWEAVE=false ztlctl session close
```

### ZTLCTL_DOCS_PATH

A special environment variable that overrides the documentation path for `ztlctl docs search`.

**When needed**: Non-editable (`pip install` without `-e`) installations where the package-relative path discovery fails to locate the `docs/` directory.

**How it works**: `ztlctl docs search` resolves docs by:
1. Checking `ZTLCTL_DOCS_PATH` first (if set and is a valid directory, uses it)
2. Falling back to a package-relative path: `<repo_root>/docs/`

**Fix for non-editable installs**:
```bash
# Point ztlctl to your local docs checkout
export ZTLCTL_DOCS_PATH=/path/to/ztlctl/docs
ztlctl docs search "your query"
```

You can persist this in your shell profile (`~/.zshrc`, `~/.bashrc`):
```bash
echo 'export ZTLCTL_DOCS_PATH=/path/to/ztlctl/docs' >> ~/.zshrc
```

---

## Real-World Examples

### Research-Heavy Vault

A vault optimized for deep research with aggressive link discovery, large agent context, and semantic search enabled:

```toml
[vault]
name = "research-vault"

[agent]
tone = "research-partner"

[agent.context]
default_budget = 16000
layer_0_min = 500
layer_1_min = 1000
layer_2_max_notes = 20
layer_3_max_hops = 2

[reweave]
enabled = true
min_score_threshold = 0.4   # Lower threshold — find more connections
max_links_per_note = 10     # Allow more links per note
lexical_weight = 0.30
tag_weight = 0.20
graph_weight = 0.30         # Prioritize graph proximity
topic_weight = 0.20

[search]
semantic_enabled = true     # Enable vector search (requires ztlctl[semantic])
embedding_model = "local"
half_life_days = 90.0       # Slower time decay for archival research
semantic_weight = 0.6       # Prefer semantic over lexical

[session]
orphan_reweave_threshold = 0.15   # More aggressive orphan connection
```

### Minimal Daily Notes Vault

A lightweight vault for daily journaling and quick capture — fast, no semantic overhead:

```toml
[vault]
name = "daily-notes"

[agent]
tone = "minimal"

[agent.context]
default_budget = 4000       # Small context for quick queries
layer_2_max_notes = 5

[reweave]
enabled = true
min_score_threshold = 0.7   # Higher threshold — only confident links
max_links_per_note = 3

[search]
semantic_enabled = false    # No vector search needed
half_life_days = 14.0       # Strong recency preference

[session]
close_reweave = true
close_integrity_check = false   # Skip integrity check for speed

[plugins.git]
enabled = true
batch_commits = true        # Commit at session close, not per-note
auto_push = false
```

---

## Complete Default Configuration

All settings with their defaults, ready to copy as a starting point:

```toml
[vault]
name = "my-vault"

[workspace]
profile = "core"

[agent]
tone = "research-partner"

[agent.context]
default_budget = 8000
layer_0_min = 500
layer_1_min = 1000
layer_2_max_notes = 10
layer_3_max_hops = 1

[reweave]
enabled = true
min_score_threshold = 0.6
max_links_per_note = 5
lexical_weight = 0.35
tag_weight = 0.25
graph_weight = 0.25
topic_weight = 0.15

[garden]
seed_age_warning_days = 7
evergreen_min_key_points = 5
evergreen_min_bidirectional_links = 3

[search]
semantic_enabled = false
embedding_model = "local"
embedding_dim = 384
half_life_days = 30.0
semantic_weight = 0.5

[ingest]
enabled = true
auto_reweave = true
default_target_type = "reference"

[session]
close_reweave = true
close_orphan_sweep = true
close_integrity_check = true
orphan_reweave_threshold = 0.2

[tags]
auto_register = true

[check]
backup_retention_days = 30
backup_max_count = 10

[plugins.git]
enabled = true
batch_commits = true
auto_push = false
auto_ignore = true
branch = "develop"
commit_style = "conventional"

[mcp]
enabled = true
transport = "stdio"

[workflow]
template = "claude-driven"
skill_set = "research"

[exports.dashboard]
include_work_queue = true
include_recent_decisions = true
include_garden_backlog = true
topic_dossier_limit = 5
```

---

## Notes

- `[vault].client` is a deprecated compatibility input. Legacy values `none` and `vanilla` map to `profile = "core"` during settings load.
- `[workspace].profile` is the canonical workspace selector. `core` is always available; additional profiles come from installed plugins.
- Core-managed paths are `ztlctl.toml`, `.ztlctl/`, `self/`, `notes/`, and `ops/`. Profile-associated scaffold paths identify workspace assets created by a profile during init, such as `.obsidian/`. Human-managed paths such as `garden/` remain outside default core indexing and mutation.
- `[plugins].obsidian` is obsolete and ignored when present in older configs. The only canonical built-in plugin config section today is `[plugins.git]`.
- URL ingestion is provider-backed. Base ztlctl supports text and markdown ingestion directly; remote fetching comes from installed source-provider plugins.
- Dashboard export still uses `--viewer` because it is a render target, not a workspace selector.


---

# Troubleshooting

## Common Issues

### "ztlctl: command not found"

**Cause**: The CLI is not on your PATH.

**Fix**:
- If installed with `pip install ztlctl`, ensure your Python scripts directory is on PATH
- If installed with `uv tool install ztlctl`, ensure `~/.local/bin` is on PATH
- Verify: `python -m ztlctl --version`

### "No vault found"

**Cause**: ztlctl must be run from a vault root (directory containing `ztlctl.toml`) or a subdirectory.

**Fix**:
```bash
# Initialize a new vault
ztlctl init my-vault
cd my-vault

# Or specify a config path
ztlctl --config /path/to/ztlctl.toml <command>
```

### "Database locked"

**Cause**: Another process has an open connection to the SQLite database.

**Fix**:
- Close other ztlctl processes or MCP server instances
- If the issue persists after closing all processes, the WAL file may be stale:
  ```bash
  # This is safe — SQLite will recover on next connection
  rm .ztlctl/ztlctl.db-wal .ztlctl/ztlctl.db-shm
  ```

### "MCP server not starting"

**Cause**: Missing the `[mcp]` optional extra.

**Fix**:
```bash
pip install ztlctl[mcp]
# or
uv tool install ztlctl --with mcp
```

### "Semantic search not available"

**Cause**: Missing the `[semantic]` optional extra.

**Fix**:
```bash
pip install ztlctl[semantic]
```

Check status: `ztlctl vector status`

### "Reweave finds no connections"

**Possible causes**:
- `min_score_threshold` is too high (default: 0.6)
- Not enough content for meaningful similarity

**Fix**:
```bash
# Lower the threshold
ZTLCTL_REWEAVE__MIN_SCORE_THRESHOLD=0.3 ztlctl reweave --auto-link-related

# Or update ztlctl.toml
# [reweave]
# min_score_threshold = 0.4
```

### Vault appears corrupted

**Fix** — rebuild the database from files:
```bash
# Full rebuild from markdown files on disk
ztlctl check --rebuild
```

If that fails, delete the database and rebuild:
```bash
rm .ztlctl/ztlctl.db
ztlctl check --rebuild
```

### Rollback to last backup

```bash
ztlctl check --rollback
```

Backups are retained for 30 days (configurable via `[check] backup_retention_days`).

### ztlctl docs search returns "docs path not found"

**Cause**: `ztlctl docs search` needs access to the `docs/` directory. In a pip-installed (non-editable) environment, the package-relative path discovery fails.

**Fix**:

```bash
# Point ztlctl to your local docs checkout
export ZTLCTL_DOCS_PATH=/path/to/ztlctl/docs
ztlctl docs search "your query"
```

This is the designed behavior — `ZTLCTL_DOCS_PATH` is the user-facing override for non-editable installs. See [Configuration > Environment Variables](configuration.md#ztlctl_docs_path) for details.

### GitHub Pages not updating after deploy

**Cause**: The GitHub Pages source must be set to "GitHub Actions" in repository settings. The default "Deploy from a branch" setting will not pick up artifact-based deployments.

**Fix**:

1. Go to your repository Settings > Pages
2. Under "Build and deployment" > Source > select **GitHub Actions**
3. Trigger a new deploy by pushing any change or re-running the docs workflow

This is a one-time setup step. Once configured, all subsequent pushes to `develop` will auto-deploy.

## Getting Help

- [GitHub Issues](https://github.com/ThatDevStudio/ztlctl/issues) — Bug reports and feature requests
- [GitHub Discussions](https://github.com/ThatDevStudio/ztlctl/discussions) — Questions and community support


---

# Best Practices

Opinionated guidance from building and maintaining ztlctl vaults. Each entry covers what goes wrong, why it happens, and the correct pattern. Cross-links point to authoritative pages for full context.

## Vault Initialization

**Always initialize with `ztlctl init`** — never create the database or config file manually.

```bash
# Correct: let init scaffold everything
ztlctl init my-vault --name "Research Notes" --profile obsidian

# Wrong: creating the directory yourself and adding files ad hoc
mkdir my-vault && touch my-vault/ztlctl.toml
```

Init writes the config, creates the SQLite index, sets up FTS5 and graph tables, and scaffolds the `self/`, `notes/`, and `ops/` directories in one atomic step. Skipping it leaves the index in an undefined state.

!!! warning "Anti-Pattern: Initializing in a deeply nested subdirectory"
    Init anchors the vault root at the chosen directory. If you initialize inside `~/work/project/docs/notes/vault/`, every relative path — CLI, config, and MCP — is relative to that anchor. Prefer `~/vaults/my-vault` or another shallow, stable location.

**Use a profile that matches your tools.** The `--profile obsidian` flag writes the Obsidian starter kit and `.obsidian/` community plugin config alongside vault scaffolding:

```bash
ztlctl init my-vault --profile obsidian  # Obsidian users
ztlctl init my-vault --profile core      # CLI-only
```

---

## Note Creation

**Choose content type before you create.** Content type cannot be changed after creation.

| Type | Use when | Initial status |
|------|----------|---------------|
| `note` | Synthesizing knowledge, recording insights | `draft` |
| `reference` | Capturing an external source (article, tool, spec) | `captured` |
| `task` | Tracking actionable work | `inbox` |

```bash
# Insight from reading → note
ztlctl create note "Transformer architecture trade-offs" --tags "ml/transformers"

# Source you read → reference
ztlctl create reference "Attention Is All You Need" --url "https://arxiv.org/abs/1706.03762" --subtype article

# Something to do → task
ztlctl create task "Implement attention visualization" --priority high
```

!!! warning "Anti-Pattern: Accumulating orphan notes"
    Notes with no outgoing links stay at `draft` status and score lower in graph-based queries. Every new note should link to at least one existing item — either explicitly with `--links "[[Target Title]]"` or by letting the Reweave plugin discover connections automatically. Run `ztlctl graph gaps` to find existing orphans.

**Use note subtypes for long-lived content.** The `knowledge` subtype signals a durable, ever-refining insight. The `decision` subtype triggers decision lifecycle rules — `proposed → accepted → superseded` — and disables auto-reweave:

```bash
ztlctl create note "GraphQL trade-offs" --subtype knowledge
ztlctl create note "Use GraphQL for nested APIs" --subtype decision
```

**Garden seeds are for raw captures, not permanent knowledge.** Seeds start at `maturity=seed` — useful for quick capture, but they age out of the work queue signal if left unattended. Promote or prune them within a week:

```bash
ztlctl garden seed "Idea: attention for code review" --tags "ml/attention"
ztlctl update ZTL-0042 --maturity budding  # once you've developed it
```

---

## Tagging

**Use `domain/scope` format for every tag.** Unscoped tags work but emit a warning and cannot be filtered with domain-prefix queries:

```bash
# Correct
--tags "lang/python,framework/fastapi,concept/async"

# Works but limited — can't filter by domain
--tags "python,fastapi,async"
```

Scoped tags unlock `--tag "lang/..."` wildcard filtering in `query list` and reweave's Jaccard overlap scoring:

```bash
ztlctl query list --tag "ml/transformers"   # exact scope
ztlctl query list --tag "ml/"              # all ml/* tags
```

**Keep domain taxonomy shallow.** Two-level (`domain/scope`) is the convention. Deeper nesting like `ml/transformers/attention` is not supported by the tag filter pattern and will be treated as a single unstructured string.

**Register tags before bulk import.** Tags are auto-registered on creation (`[tags] auto_register = true`). If you bulk-import content with non-standard tags and then rename, the old tags remain as index artifacts. Decide your taxonomy before large imports.

---

## Linking and Reweave

**Let reweave suggest connections — don't maintain all links manually.** The 4-signal scoring algorithm (BM25 35%, Jaccard tags 25%, graph proximity 25%, topic 15%) surfaces connections you would miss by hand. Review suggestions in sessions:

```bash
ztlctl reweave --dry-run           # preview what would be linked
ztlctl reweave --auto-link-related # apply discovered connections
```

**Tune thresholds only after your vault has 50+ notes.** The default `min_score_threshold = 0.6` is calibrated for medium-density vaults. Early vaults have sparse graphs and noisy BM25 scores — tuning too early optimizes for noise:

```toml
# ztlctl.toml — tune after 50+ notes
[reweave]
min_score_threshold = 0.6   # default: start here
max_links_per_note = 5      # default: raise for denser graphs
```

For high-quality, low-noise links:

```toml
[reweave]
min_score_threshold = 0.75
max_links_per_note = 3
```

!!! warning "Anti-Pattern: Disabling reweave globally when you only want to skip one note"
    `[reweave] enabled = false` turns off auto-reweave for all new content. Use `--no-reweave` to skip a single creation:

    ```bash
    ztlctl create note "Quick scratchpad" --no-reweave
    ```

**Decision notes are intentionally excluded from auto-reweave.** If you want to connect a decision note, run reweave manually:

```bash
ztlctl reweave --id ZTL-0042
```

---

## Session Management

**Close sessions after completing a work unit.** Sessions are operational coordination state. Leaving a session open blocks starting a new one and delays the session-close enrichment pipeline:

```bash
ztlctl agent session start "OAuth security research"
# ... do work ...
ztlctl agent session close --summary "Mapped OAuth 2.0 attack surface"
```

The close triggers reweave, orphan sweep, integrity check, and graph materialization — this is where connections are stitched together across the session's work.

!!! warning "Anti-Pattern: Running multiple concurrent sessions"
    Only one session can be open at a time. If you try to start a session while one is open, ztlctl returns an error. Close the existing session first:

    ```bash
    ztlctl agent session close --summary "Pausing work"
    ztlctl agent session start "New topic"
    ```

**Log reasoning during sessions, not just captures.** `session log` entries feed context assembly and decision extraction. Pinned entries persist in the work queue:

```bash
ztlctl agent session log "Key insight: token replay risk via HTTP" --pin
```

**Read the session-close enrichment report.** The JSON close result tells you how many new links were discovered and whether orphans were connected:

```bash
ztlctl agent session close --summary "Done" --json
# → reweave_count, orphan_count, integrity_issues
```

---

## Plugin Configuration

**Start with `batch_commits = true` and `auto_push = false`.** The git plugin's defaults are safe but aggressive — `auto_push = true` by default will push to remote on every session close:

```toml
[plugins.git]
batch_commits = true    # one commit per session close (recommended)
auto_push = false       # push manually when ready
```

Enable auto-push only when you're confident in your session discipline and your remote is always available.

**Understand batch mode before enabling it.** In batch mode, notes created outside an active session are staged but never committed — the commit trigger is `session close`. If you use ztlctl without sessions, switch to immediate mode:

```toml
[plugins.git]
batch_commits = false   # commit immediately after each operation
```

**Never edit vault files directly in git.** Bypass the CLI and all index state breaks — FTS5, the graph, tag index, and content ID counter all live in the SQLite database. Direct file edits create orphaned files that `check --rebuild` must recover:

```bash
# Never: vim notes/ztl_a1b2c3d4.md (edits bypass the DB)
# Always: ztlctl update ZTL-0001 --body "Updated content"
```

---

## Agent Workflows

**Always wrap agent work in a session.** Sessions provide the operational boundary for enrichment. Without a session, auto-reweave still fires per-note, but cross-session reweave, orphan sweep, and graph materialization do not run until a session closes:

```bash
# Agent start
ztlctl agent session start "Literature review: attention mechanisms"
# ... create notes and references ...
# Agent end
ztlctl agent session close --summary "Surveyed 12 papers, 5 synthesis notes"
```

**Check `.success` before proceeding on every ServiceResult.** Every CLI command returns a JSON envelope with `success`, `data`, and `meta`. Agents that ignore the success field risk chaining operations on failed state:

```bash
RESULT=$(ztlctl create note "My Note" --json)
echo $RESULT | jq '.success'  # check before using .data.id
```

**Use `.recovery` hints on failure.** Service errors include a `recovery` field that names the corrective action. Parse it before retrying or escalating:

```json
{
  "success": false,
  "error": {"message": "Vault not initialized", "recovery": "run ztlctl init"}
}
```

**Use `agent context` to orient before creating.** Fetch topic context before creating notes to avoid duplicates and to pick up related content for linking:

```bash
ztlctl agent context --topic "distributed-systems" --budget 4000 --json
```

**Use `--json` for all agent operations.** Rich terminal output is not machine-parseable. Pass `--json` to get structured ServiceResult envelopes on stdout:

```bash
ztlctl create note "Title" --json
ztlctl query search "topic" --json
ztlctl agent session close --json
```

---

## What Not to Do — Summary Table

| Anti-Pattern | Impact | Fix |
|-------------|--------|-----|
| Bare tags (`python` instead of `lang/python`) | Cannot filter by domain; poor reweave scoring | Use `domain/scope` format |
| Orphan notes (no outgoing links) | Graph gaps, missed connections, lower search rank | Always link on create or run `ztlctl reweave` |
| Direct file edits bypassing CLI | Breaks FTS5, graph, tag index, counters | Use `ztlctl update` and `ztlctl archive` |
| Manual DB edits | Corruption, broken FK constraints | Never touch `ztlctl.db` directly |
| Opening sessions without closing | Blocks new sessions; delays enrichment pipeline | Close all sessions before starting new work |
| Wrong content type | Cannot be changed post-creation | Match type to purpose before creating |
| `auto_push = true` without session discipline | Unexpected remote pushes mid-work | Set `auto_push = false` until workflow is stable |
| `--no-reweave` globally disabled | No automatic link discovery | Use `--no-reweave` per-note, not global disable |
| Creating notes without session (agent) | No cross-session enrichment | Wrap all agent work in `session start / close` |
| Ignoring `success: false` in JSON output | Chaining ops on failed state | Always check `.success` before using `.data` |

---

## Next Steps

- [Tutorial](tutorial.md) — step-by-step walkthrough of vault setup through export
- [Configuration](configuration.md) — full `ztlctl.toml` reference with all thresholds and defaults
- [Agentic Workflows](agentic-workflows.md) — session discipline, recipe walkthroughs, and MCP integration for agents
- [Built-in Plugins](plugins.md) — git and reweave plugin configuration deep-dive


---

# Developer Guide

# Developer Guide

The Developer Guide is for contributors and integrators — those building plugins, extending ztlctl's capabilities, or integrating it into agent systems via MCP.

## In This Guide

| Page | What it covers |
|------|----------------|
| [Contributing](../development.md) | Architecture overview, branch workflow, and contribution process |
| [Plugin Authoring](../plugin-guide.md) | Tutorial and hookspec reference for plugin authors — build your first plugin |
| [API Reference](../api-reference.md) | Auto-generated API reference for all public plugin contracts and the ActionRegistry |
| [MCP Server](../mcp.md) | MCP tools, resources, prompts, and agent integration patterns |
| [Agent System Manual](../agents.md) | Machine-readable schemas, state machines, and interaction flows for LLM consumers |


---

# Development

## Setup

```bash
# Clone the repository
git clone https://github.com/ThatDevStudio/ztlctl.git
cd ztlctl

# Install all development dependencies
uv sync --group dev

# Verify the installation
uv run ztlctl --version

# Run the test suite
uv run pytest
```

## Development Commands

```bash
uv run ztlctl --help                             # Run the CLI
uv run pytest --cov --cov-report=term-missing    # Tests with coverage
uv run ruff check .                              # Lint
uv run ruff format .                             # Format
uv run mypy src/                                 # Type check
uv run pre-commit run --all-files                # All pre-commit hooks
```

## CI/CD Pipeline

GitHub Actions exposes two CI/CD workflows:

- `PR CI` runs on pull requests to `develop` and exposes the required `Validate PR` check.
- `Release Pipeline` runs only after changes land on `develop`, then performs merge validation,
  release, publish, and Homebrew sync as dependent jobs.

`Validate PR` covers lint, format, type checking, package build, security audit, the full pytest
suite, the MCP stdio integration test, and commit lint. `Validate Merge` in the release pipeline
re-runs the merge validation after landing on `develop` and adds the semantic CI smoke test using
the lightweight internal `semantic-ci` dependency group.

The release workflow builds `dist/release-manifest.json` and treats it as the source of truth
for release version, tag, asset path, download URL, and source tarball hash. Downstream publish
and Homebrew steps consume that manifest instead of rediscovering release metadata.

## Homebrew Formula

The Homebrew tap publishes `ztlctl` at `ThatDev/ztlctl`.

```bash
# Build release assets for an existing tag
python3 scripts/build_release_manifest.py --release-tag v1.7.1 --output dist/release-manifest.json

# Generate the Homebrew formula from the release manifest
python3 scripts/update_homebrew_formula.py --manifest dist/release-manifest.json --output dist/ztlctl.rb
```

The formula is now a derived release artifact rather than a checked-in source file. It uses the
release tarball as the stable source, installs the CLI into a Python virtualenv, and derives both
runtime and build-backend resources from the repository's `uv.lock`.

## Architecture

ztlctl follows a strict 6-layer package structure where dependencies flow downward:

```
commands → output → services → config/infrastructure → domain
```

```
src/ztlctl/
├── domain/          # Types, enums, lifecycle rules, ID patterns
├── infrastructure/  # SQLite/SQLAlchemy, NetworkX graph, filesystem
├── config/          # Pydantic config models, TOML discovery
├── services/        # Business logic (create, query, graph, reweave, ...)
├── output/          # Rich/JSON formatters
├── commands/        # Click CLI commands
├── plugins/         # Pluggy hook specs and built-in plugins
├── mcp/             # MCP server adapter
└── templates/       # Jinja2 templates for content creation
```

For the complete internal design specification (architecture decisions, invariants, implementation details), see [DESIGN.md](https://github.com/ThatDevStudio/ztlctl/blob/develop/DESIGN.md) in the repository.

### Action Model

ztlctl uses a 4-layer action model that connects user-facing surfaces (CLI, MCP) to service logic:

| Layer | Components | Role |
|-------|-----------|------|
| **Data** | `ActionParam`, `ActionDefinition` | Frozen dataclasses describing an action's name, params, side effect, and surface metadata |
| **Service** | `*Service` classes | Business logic that executes the action and returns a `ServiceResult` |
| **Controller** | `BaseController` subclasses | Wires one `ActionDefinition` to one `Service` call; handles input coercion and error wrapping |
| **Registry** | `ActionRegistry` (singleton) | Indexed map of all registered `ActionDefinition` objects; queried at startup |

**How CLI auto-generation works:**
At startup, the CLI generator calls `get_action_registry().list_actions(category=...)` for each command group. It iterates the returned `ActionDefinition` objects and builds Click commands from `ActionParam` metadata (`cli_is_argument`, `cli_flag`, `cli_multiple`, `cli_name`). No code generation — the CLI surface is driven entirely by the registry at import time.

**How MCP auto-generation works:**
The MCP adapter calls `get_action_registry().list_actions()` and converts each `ActionDefinition` into an MCP tool descriptor using `mcp_when_to_use`, `mcp_avoid_when`, `mcp_common_errors`, and `mcp_example` fields. Same registry, different surface formatter.

**Plugin integration points:**
1. `pre_action` fires before the Controller calls the Service — plugins can abort (return `ActionRejection`) or transform kwargs
2. `post_action` fires after the Controller returns the `ServiceResult` — all plugins receive the call regardless of success/failure
3. `register_note_types()` returns `NoteTypeDefinition` objects — PluginManager creates `ActionDefinition` entries in the registry for CRUD operations on each custom type
4. `register_content_models()` extends the `CONTENT_REGISTRY` so new note subtypes are recognized by `CreateService`

**ServiceResult contract:**
Every Service method returns `ServiceResult`. Controllers unwrap it to emit exit codes (CLI) or structured responses (MCP). Plugins receive the raw `ServiceResult` via `post_action(result=...)`.

## Template Overrides

Vault-specific Jinja2 overrides can live under `.ztlctl/templates/`.

- Self-document templates: `.ztlctl/templates/self/identity.md.j2` or `.ztlctl/templates/identity.md.j2`
- Content body templates: `.ztlctl/templates/content/note.md.j2` or `.ztlctl/templates/note.md.j2`

ztlctl checks those override paths first and falls back to the bundled package templates when no user template exists.

## Workflow Templates

`ztlctl workflow init` and `ztlctl workflow update` scaffold vault workflow guidance using a packaged Copier template.

- Choices: source control (`git|none`), profile (dynamic installed profile ids, with deprecated `none` and `vanilla` aliases resolving to `core`), workflow (`claude-driven|agent-generic|manual`), skill set (`research|engineering|minimal`)
- Answers file: `.ztlctl/workflow-answers.yml`
- Generated guidance: `.ztlctl/workflow/`

`ztlctl init` applies the default workflow scaffold automatically unless `--no-workflow` is passed.

## Plugin Init Hooks

Plugins can now contribute ordered init steps through `register_vault_init_steps()`.

- Each step receives a normalized `VaultInitContext`
- Steps can create files, emit warnings, and return structured setup instructions
- `ztlctl init` aggregates those instructions into the result payload and prints them under `Next steps`
- The first-party Obsidian profile plugin uses this surface to scaffold `.obsidian/`, seed `garden/`, and print plugin-install guidance before handing `.obsidian/` off to the user for further customization

Legacy `WorkspaceProfileContribution.init_scaffold` remains supported temporarily and is wrapped into the same ordered init pipeline for compatibility.

## Further Reading

- [Plugin Authoring Guide](plugin-guide.md) — build plugins with hookspecs, custom note types, and capability declarations
- [API Reference](api-reference.md) — auto-generated reference for `ActionDefinition`, `ActionRegistry`, hookspecs, and plugin contracts

## Contributing

See [CONTRIBUTING.md](https://github.com/ThatDevStudio/ztlctl/blob/develop/CONTRIBUTING.md) for the full contribution guide, including:

- Branching model and PR workflow
- Conventional commit format
- Pre-submit checklist
- Code standards and dependency management
- Dependency management (always use `uv add`, never `uv pip install`)


---

# Plugin Authoring Guide

ztlctl's plugin system is built on [pluggy](https://pluggy.readthedocs.io/). Plugins hook into lifecycle events, extend the CLI and MCP server, contribute custom note types, and declare security capabilities. Plugins are discovered automatically at startup via Python entry points (`importlib.metadata`).

This guide covers:

1. [Tutorial: Build Your First Plugin](#tutorial-build-your-first-plugin) — step-by-step walkthrough
2. [Hookspec Reference](#hookspec-reference) — all 16 active hookspecs with exact signatures and behavior
3. [Plugin Metadata](#plugin-metadata-pluginmetadata) — marketplace and discoverability metadata
4. [Compatibility and Versioning](#compatibility-and-versioning) — API version contract

---

## Tutorial: Build Your First Plugin

### 1. Create the Plugin Package

Create a minimal Python package:

```
my_vault_plugin/
    __init__.py
pyproject.toml
```

`pyproject.toml`:

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-vault-plugin"
version = "0.1.0"
dependencies = [
    "pluggy>=1.3",
    "pydantic>=2.0",
]
```

In `my_vault_plugin/__init__.py`, import the hook marker:

```python
import pluggy

hookimpl = pluggy.HookimplMarker("ztlctl")
```

All hook implementations must be decorated with `@hookimpl`. The marker name must be `"ztlctl"` to match the hook specification namespace.

### 2. Add PLUGIN_API_VERSION

Declare the API version your plugin targets at class level:

```python
class MyVaultPlugin:
    PLUGIN_API_VERSION = 1
```

The current host API version is `1`. Plugins declaring `PLUGIN_API_VERSION = 1` are fully compatible. See [Compatibility and Versioning](#compatibility-and-versioning) for the compatibility window rules.

Missing `PLUGIN_API_VERSION` is allowed — legacy plugins load without a warning but receive no compatibility guarantees.

### 3. Implement post_action

`post_action` is the primary hook for reacting to vault events. All registered plugins receive every call; filter on `action_name` to scope your plugin's logic:

```python
import pluggy
from typing import Any

hookimpl = pluggy.HookimplMarker("ztlctl")


class MyVaultPlugin:
    PLUGIN_API_VERSION = 1

    @hookimpl
    def post_action(
        self,
        action_name: str,
        kwargs: dict[str, Any],
        result: Any,
    ) -> None:
        if action_name != "create_note":
            return

        # result is the ServiceResult object (or None on the EventBus bridge path)
        if result is not None and (not hasattr(result, "ok") or not result.ok):
            return  # skip failed actions

        content_id = kwargs.get("content_id", "")
        title = kwargs.get("title", "")
        print(f"New note created: [{content_id}] {title}")
```

Common `action_name` values:

| Action Name | Triggered by |
|---|---|
| `create_note` | `ztlctl create note` |
| `create_reference` | `ztlctl create reference` |
| `create_task` | `ztlctl create task` |
| `update` | `ztlctl update` |
| `close` / `archive` | `ztlctl close` / `ztlctl archive` |
| `session_start` | `ztlctl session start` |
| `session_close` | `ztlctl session close` |
| `reweave` | `ztlctl reweave` |
| `check` | `ztlctl check` |
| `init` | `ztlctl init` |

### 4. Declare Capabilities

Plugins that access sensitive resources must declare their capabilities:

```python
@hookimpl
def declare_capabilities(self) -> set[str]:
    return {"network"}
```

Valid capability identifiers:

| Capability | Meaning |
|---|---|
| `filesystem` | Reads or writes files outside the vault |
| `network` | Makes outbound network requests |
| `database` | Directly accesses the vault SQLite database |
| `git` | Runs git subprocess commands |

The PluginManager emits a warning if a plugin uses a capability without declaring it. In a future API version, undeclared capabilities may be blocked.

### 5. Add a Custom Note Type (NoteTypeDefinition)

Plugins can register entirely new note types with their own lifecycle, template, and required structure:

```python
from ztlctl.domain.content import NoteModel
from ztlctl.domain.registry import NoteTypeDefinition


class MyVaultPlugin:
    PLUGIN_API_VERSION = 1

    @hookimpl
    def register_note_types(self) -> list[NoteTypeDefinition]:
        sprint_type = NoteTypeDefinition(
            name="sprint",
            content_type="note",
            model_cls=NoteModel,
            transitions={
                "active": ["completed", "cancelled"],
                "completed": [],
                "cancelled": [],
            },
            template_name="sprint.md.j2",
            required_sections=["## Goal", "## Stories"],
        )
        return [sprint_type]
```

`NoteTypeDefinition` fields:

| Field | Type | Description |
|---|---|---|
| `name` | `str` | Unique registry key (e.g. `"sprint"`) |
| `content_type` | `str` | Parent type: `"note"`, `"reference"`, `"task"`, or `"log"` |
| `model_cls` | `type[ContentModel]` | Pydantic model class for validation |
| `transitions` | `dict[str, list[str]]` | Status transition map; every target must also be a key |
| `template_name` | `str` | Jinja2 template filename (empty string for DB-only types) |
| `required_sections` | `list[str]` | Markdown sections required during validation |
| `initial_status` | `str` | Status on creation; empty means "use first key of transitions" |
| `is_subtype` | `bool` | `True` for subtypes of existing content types |
| `parent_type` | `str \| None` | Required when `is_subtype=True` |

PluginManager auto-creates `create`, `update`, and `close` ActionDefinitions for each registered `NoteTypeDefinition` and adds them to the ActionRegistry. The CLI and MCP generators pick them up automatically.

### 6. Add Plugin Config (get_config_schema + initialize)

Plugins can declare a Pydantic config schema so users configure the plugin in `.ztlctl/config.toml`:

```python
from pydantic import BaseModel


class MyPluginConfig(BaseModel):
    webhook_url: str = ""
    enabled: bool = True


class MyVaultPlugin:
    PLUGIN_API_VERSION = 1

    def __init__(self) -> None:
        self._config = MyPluginConfig()

    @hookimpl
    def get_config_schema(self) -> type[BaseModel]:
        return MyPluginConfig

    @hookimpl
    def initialize(self, config: BaseModel | None) -> None:
        if config is not None:
            self._config = config  # type: ignore[assignment]
```

Matching section in `.ztlctl/config.toml`:

```toml
[plugins.my-plugin]
webhook_url = "https://hooks.example.com/ztlctl"
enabled = true
```

`get_config_schema()` is called once at plugin load time. If a matching `[plugins.<name>]` TOML section exists, its contents are validated against the returned model and then passed to `initialize()`. If no config section exists, `initialize()` receives `None`.

### 7. Register via Entry Point

Register the plugin class using a Python entry point in `pyproject.toml`:

```toml
[project.entry-points."ztlctl.plugins"]
my-plugin = "my_vault_plugin:MyVaultPlugin"
```

ztlctl discovers all plugins registered under the `ztlctl.plugins` entry-point group using `importlib.metadata` at startup. The entry-point name (here `my-plugin`) is used as the plugin's config section name and display name. Install the package (`pip install -e .`) and ztlctl will load it automatically.

### 8. Test Your Plugin

Plugins can be tested directly without a running vault:

```python
import pytest
from my_vault_plugin import MyVaultPlugin


def test_post_action_create_note(capsys: pytest.CaptureFixture[str]) -> None:
    plugin = MyVaultPlugin()

    # Simulate a successful create_note action
    plugin.post_action(
        action_name="create_note",
        kwargs={"content_id": "20240101120000", "title": "My First Note"},
        result=None,  # None = pass-through on the EventBus bridge path
    )

    captured = capsys.readouterr()
    assert "20240101120000" in captured.out
    assert "My First Note" in captured.out


def test_post_action_ignores_other_actions() -> None:
    plugin = MyVaultPlugin()

    # Should be a no-op for other action names
    plugin.post_action(
        action_name="reweave",
        kwargs={},
        result=None,
    )
    # No assertion needed — just verify no exception is raised
```

Hooks can be tested without pluggy infrastructure by calling them directly as methods. Use `result=None` to simulate the EventBus bridge path (pass-through).

---

## Complete MyVaultPlugin Example

```python
"""my_vault_plugin/__init__.py — complete working example."""

from __future__ import annotations

from typing import Any

import pluggy
from pydantic import BaseModel

from ztlctl.domain.content import NoteModel
from ztlctl.domain.registry import NoteTypeDefinition

hookimpl = pluggy.HookimplMarker("ztlctl")


class MyPluginConfig(BaseModel):
    webhook_url: str = ""
    enabled: bool = True


class MyVaultPlugin:
    """Example ztlctl plugin demonstrating core plugin patterns."""

    PLUGIN_API_VERSION = 1

    def __init__(self) -> None:
        self._config = MyPluginConfig()

    # ── Security ──────────────────────────────────────────────────────────

    @hookimpl
    def declare_capabilities(self) -> set[str]:
        return {"network"}

    # ── Configuration ─────────────────────────────────────────────────────

    @hookimpl
    def get_config_schema(self) -> type[BaseModel]:
        return MyPluginConfig

    @hookimpl
    def initialize(self, config: BaseModel | None) -> None:
        if config is not None:
            self._config = config  # type: ignore[assignment]

    # ── Lifecycle events ──────────────────────────────────────────────────

    @hookimpl
    def post_action(
        self,
        action_name: str,
        kwargs: dict[str, Any],
        result: Any,
    ) -> None:
        if action_name != "create_note":
            return
        if result is not None and (not hasattr(result, "ok") or not result.ok):
            return

        if not self._config.enabled or not self._config.webhook_url:
            return

        content_id = kwargs.get("content_id", "")
        title = kwargs.get("title", "")
        print(f"Webhook: new note [{content_id}] {title} -> {self._config.webhook_url}")

    # ── Custom note types ─────────────────────────────────────────────────

    @hookimpl
    def register_note_types(self) -> list[NoteTypeDefinition]:
        return [
            NoteTypeDefinition(
                name="sprint",
                content_type="note",
                model_cls=NoteModel,
                transitions={
                    "active": ["completed", "cancelled"],
                    "completed": [],
                    "cancelled": [],
                },
                template_name="sprint.md.j2",
                required_sections=["## Goal", "## Stories"],
            )
        ]
```

`pyproject.toml` for the complete plugin:

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-vault-plugin"
version = "0.1.0"
dependencies = ["pluggy>=1.3", "pydantic>=2.0"]

[project.entry-points."ztlctl.plugins"]
my-plugin = "my_vault_plugin:MyVaultPlugin"

[tool.ztlctl-plugin]
name = "my-vault-plugin"
version = "0.1.0"
author = "Your Name"
description = "Example ztlctl plugin"
ztlctl_api_version = 1
capabilities = ["network"]
```

---

## Hookspec Reference

All 16 active hookspecs are defined in the `ZtlctlHookSpec` class in `src/ztlctl/plugins/hookspecs.py`. Implement only the hooks your plugin needs — unimplemented hooks are no-ops. See also the [API Reference](api-reference.md) for auto-generated signatures and docstrings.

Every implementation must be decorated with:

```python
import pluggy
hookimpl = pluggy.HookimplMarker("ztlctl")
```

---

### Generic Action Hooks (Preferred)

These two hooks cover the full action lifecycle. Prefer them over the deprecated per-event hooks.

#### `pre_action`

```python
def pre_action(
    self, action_name: str, kwargs: dict[str, Any]
) -> ActionRejection | dict[str, Any] | None:
    ...
```

- `firstresult=True` — the first plugin returning a non-`None` value wins; subsequent plugins are not called
- Return `ActionRejection(reason=..., code=..., detail={})` to abort the action before it runs
- Return a modified `kwargs` dict to transform inputs before the action handler runs
- Return `None` (or do not implement) to pass through unchanged
- Fires before every registered action

`ActionRejection` fields:

| Field | Type | Default | Description |
|---|---|---|---|
| `reason` | `str` | — | Human-readable rejection explanation |
| `code` | `str` | `"plugin_rejected"` | Machine-readable error code |
| `detail` | `dict[str, Any]` | `{}` | Optional structured context |

Example — rate-limit note creation:

```python
from ztlctl.plugins.contracts import ActionRejection

@hookimpl
def pre_action(
    self, action_name: str, kwargs: dict[str, Any]
) -> ActionRejection | dict[str, Any] | None:
    if action_name == "create_note" and self._rate_limit_exceeded():
        return ActionRejection(
            reason="Rate limit exceeded — too many notes created today",
            code="rate_limit_exceeded",
            detail={"limit": 50},
        )
    return None
```

#### `post_action`

```python
def post_action(self, action_name: str, kwargs: dict[str, Any], result: Any) -> None:
    ...
```

- `firstresult=False` — all registered plugins receive this call
- Called after every action regardless of outcome
- Filter on `action_name` to scope your plugin's behavior
- `result` is the `ServiceResult` object, or `None` on the EventBus bridge path
- Return value is ignored

---

### Plugin Lifecycle Hooks

#### `get_config_schema`

```python
def get_config_schema(self) -> type[BaseModel] | None:
    ...
```

- `firstresult=True`
- Return a Pydantic `BaseModel` **class** (not an instance) to declare the plugin's config schema
- Called once at plugin load time
- Return `None` or do not implement if the plugin needs no configuration

#### `initialize`

```python
def initialize(self, config: BaseModel | None) -> None:
    ...
```

- Called once after `get_config_schema()`, with the validated config instance (or `None` if no config)
- Store the config reference on `self` for use in other hooks
- If `get_config_schema()` returns `None`, `config` will always be `None`

---

### Extension Contribution Hooks

These hooks let plugins add content to various ztlctl subsystems. All are `firstresult=False` — every plugin's contributions are collected and merged.

| Hook | Return Type | What It Contributes |
|---|---|---|
| `register_content_models` | `dict[str, type[ContentModel]] \| None` | Subtype-to-ContentModel mappings added to `CONTENT_REGISTRY` |
| `register_cli_commands` | `list[CliCommandContribution] \| None` | Additional Click commands exposed under `ztlctl` |
| `register_mcp_tools` | `list[McpToolContribution] \| None` | MCP tool handlers available via `ztlctl serve` |
| `register_mcp_resources` | `list[McpResourceContribution] \| None` | MCP resource URIs available via `ztlctl serve` |
| `register_mcp_prompts` | `list[McpPromptContribution] \| None` | MCP prompt templates available via `ztlctl serve` |
| `register_workflow_modules` | `list[WorkflowModuleContribution] \| None` | Workflow export module renderers |
| `register_workspace_profiles` | `list[WorkspaceProfileContribution] \| None` | Workspace profile definitions for `ztlctl init` |
| `register_vault_init_steps` | `list[VaultInitStepContribution] \| None` | Ordered steps injected into the `ztlctl init` pipeline |
| `register_source_providers` | `list[SourceProviderContribution] \| None` | Source acquisition providers for content ingestion |
| `register_note_types` | `list[NoteTypeDefinition] \| None` | Custom note types with lifecycle and CRUD auto-generation |
| `register_render_contributions` | `list[RenderContribution] \| None` | Rich terminal and MCP formatters for custom note types |

Return `None` (or do not implement) to contribute nothing.

#### Contribution Contract Types

Contribution dataclasses live in `ztlctl.plugins.contracts`:

**`CliCommandContribution`**

```python
@dataclass(frozen=True)
class CliCommandContribution:
    name: str
    command: click.Command
```

**`McpToolContribution`**

```python
@dataclass(frozen=True)
class McpToolContribution:
    name: str
    handler: Callable[..., dict[str, Any]]
    catalog_entry: ToolCatalogEntry
```

**`McpResourceContribution`**

```python
@dataclass(frozen=True)
class McpResourceContribution:
    uri: str
    description: str
    handler: Callable[[Any], Any]
```

**`McpPromptContribution`**

```python
@dataclass(frozen=True)
class McpPromptContribution:
    name: str
    description: str
    handler: Callable[..., str]
    takes_vault: bool = True
```

**`WorkflowModuleContribution`**

```python
@dataclass(frozen=True)
class WorkflowModuleContribution:
    name: str
    render: Callable[[dict[str, Any]], str]
```

**`WorkspaceProfileContribution`**

```python
@dataclass(frozen=True)
class WorkspaceProfileContribution:
    profile_id: str
    description: str
    aliases: tuple[str, ...] = ()
    managed_paths: tuple[str, ...] = ()
    init_scaffold: Callable[[Path], list[str]] | None = None
```

**`VaultInitStepContribution`**

```python
@dataclass(frozen=True)
class VaultInitStepContribution:
    step_id: str
    description: str
    run: Callable[[VaultInitContext], VaultInitStepResult]
    order: int = 500
    profiles: tuple[str, ...] = ()
```

Steps with lower `order` run first. `profiles` restricts the step to named workspace profiles; empty tuple means the step runs for all profiles.

**`SourceProviderContribution`**

```python
@dataclass(frozen=True)
class SourceProviderContribution:
    name: str
    description: str
    schemes: tuple[str, ...]
    fetch: Callable[[SourceFetchRequest], SourceFetchResult]
```

**`RenderContribution`**

```python
@dataclass(frozen=True)
class RenderContribution:
    note_type: str
    rich_formatter: Callable[[dict[str, Any]], str]
    mcp_formatter: Callable[[dict[str, Any]], dict[str, Any]]
```

---

### Security — Capability Declarations

#### `declare_capabilities`

```python
def declare_capabilities(self) -> set[str] | None:
    ...
```

Return the set of capabilities this plugin uses:

```python
@hookimpl
def declare_capabilities(self) -> set[str]:
    return {"filesystem", "network"}
```

Valid values: `{"filesystem", "network", "database", "git"}`.

Return `None` or do not implement to indicate no declaration. Missing declarations generate a warning in plugin API v2. Future API versions may enforce declarations and refuse to load undeclared plugins.

---

### Deprecated Per-Event Hooks

!!! warning "Deprecated Hookspecs"
    The following hookspecs still work but are deprecated since plugin API v2. Use `pre_action` / `post_action` instead:

    - `post_create`, `post_update`, `post_close`, `post_reweave`
    - `post_session_start`, `post_session_close`
    - `post_check`, `post_init`, `post_init_profile`

    These emit `DeprecationWarning` when implemented and will be removed in a future API version.

The following hooks are deprecated since plugin API v2. They emit `DeprecationWarning` when implemented. Use `post_action` with `action_name` filtering instead.

| Deprecated Hook | Signature | Migrate to |
|---|---|---|
| `post_create` | `(content_type: str, content_id: str, title: str, path: str, tags: list[str]) -> None` | `post_action` + filter `action_name in {"create_note", "create_reference", "create_task", "create_log"}` |
| `post_update` | `(content_type: str, content_id: str, fields_changed: list[str], path: str) -> None` | `post_action` + filter `action_name.startswith("update_")` |
| `post_close` | `(content_type: str, content_id: str, path: str, summary: str) -> None` | `post_action` + filter `action_name.endswith("_close")` |
| `post_reweave` | `(source_id: str, affected_ids: list[str], links_added: int) -> None` | `post_action` + filter `action_name == "reweave"` |
| `post_session_start` | `(session_id: str) -> None` | `post_action` + filter `action_name == "session_start"` |
| `post_session_close` | `(session_id: str, stats: dict[str, Any]) -> None` | `post_action` + filter `action_name == "session_close"` |
| `post_check` | `(issues_found: int, issues_fixed: int) -> None` | `post_action` + filter `action_name == "check"` |
| `post_init` | `(vault_name: str, client: str, tone: str) -> None` | `post_action` + filter `action_name == "init"` |
| `post_init_profile` | `(vault_name: str, profile: str, tone: str, managed_paths: list[str]) -> None` | `post_action` + filter `action_name == "init_profile"` |

These hooks will be removed in a future API version. Migration is straightforward: replace per-event implementations with a single `post_action` and filter by `action_name`.

---

## Plugin Metadata (PluginMetadata)

For marketplace listing and future discoverability, declare plugin metadata in `pyproject.toml`:

```toml
[tool.ztlctl-plugin]
name = "my-vault-plugin"
version = "1.0.0"
author = "Your Name"
description = "What this plugin does"
ztlctl_api_version = 1
capabilities = ["network"]
```

This corresponds to the `PluginMetadata` dataclass in `ztlctl.plugins.contracts`:

```python
@dataclass(frozen=True)
class PluginMetadata:
    name: str
    version: str
    author: str
    capabilities: tuple[str, ...]
    ztlctl_api_version: int
    description: str = ""
```

Note: `capabilities` in `PluginMetadata` refers to contribution hook identifiers (e.g. `"register_note_types"`, `"register_cli_commands"`), not security capability identifiers like `"network"`. Use `declare_capabilities()` for security declarations and `PluginMetadata.capabilities` for feature surface declarations.

---

## Compatibility and Versioning

| Item | Value |
|---|---|
| Current `PLUGIN_API_VERSION` | `1` |
| Compatibility window | `2` (versions within 2 of current load with a deprecation warning) |
| Minimum supported version | `PLUGIN_API_VERSION - window` (exclusive) |

**Rules:**

- `PLUGIN_API_VERSION = 1` (current): fully compatible, loads without warning
- Plugin version within the window but below current: loads with a deprecation warning
- Plugin version equal to or below the compatibility floor (`current - window`): rejected with `PluginLoadError`
- Plugin version above `PLUGIN_API_VERSION`: rejected with `PluginLoadError` ("please upgrade ztlctl")
- No `PLUGIN_API_VERSION` attribute: treated as a legacy plugin; loads without warning but receives no compatibility guarantees

Example with current API version `1` and window `2`:

| Plugin declares | Result |
|---|---|
| `PLUGIN_API_VERSION = 1` | Loads, no warning |
| `PLUGIN_API_VERSION = 2` | Rejected — plugin requires a newer host |
| No attribute | Loads, no warning (legacy) |

```python
# Compatibility check logic (simplified from _version.py)
from ztlctl.plugins._version import PLUGIN_API_VERSION, PluginLoadError

COMPATIBILITY_WINDOW = 2

declared = getattr(plugin, "PLUGIN_API_VERSION", None)
if declared is None:
    pass  # legacy — allowed
elif declared > PLUGIN_API_VERSION:
    raise PluginLoadError("Plugin requires newer ztlctl version")
elif declared <= PLUGIN_API_VERSION - COMPATIBILITY_WINDOW:
    raise PluginLoadError("Plugin API version too old — update the plugin")
elif declared < PLUGIN_API_VERSION:
    print("Warning: plugin is within compatibility window but not current")
```


---

# API Reference

Auto-generated from Python source using [mkdocstrings](https://mkdocstrings.github.io/python/).
All signatures and docstrings reflect the current `src/ztlctl/` codebase.

This page documents the stable public API surface: plugin hookspecs, contribution contracts, the action system, and API versioning. Use the section headings to navigate to the module you need. For usage examples and step-by-step tutorials, see the [Plugin Authoring Guide](plugin-guide.md).

!!! note "Scope"
    This reference covers the **plugin public API** only — the contracts, hookspecs, and action system
    that plugin authors and advanced integrators interact with. Internal service and infrastructure
    layers are not documented here.

## Plugin Hookspecs

The `ZtlctlHookSpec` class defines all pluggy hookspecs. Implement any subset of these in your plugin class.

::: ztlctl.plugins.hookspecs
    options:
      show_root_heading: true
      heading_level: 3
      members_order: source
      show_source: true
      show_signature_annotations: true
      separate_signature: true
      filters:
        - "!^_"

## Plugin Contracts

Data classes returned from and passed to hookspecs.

::: ztlctl.plugins.contracts
    options:
      show_root_heading: true
      heading_level: 3
      members_order: source
      show_source: false
      show_signature_annotations: true

## API Versioning

::: ztlctl.plugins._version
    options:
      show_root_heading: true
      heading_level: 3
      show_source: false
      members_order: source
      filters:
        - "!^_COMPATIBILITY"

## Action System

`ActionDefinition` and `ActionParam` are the frozen dataclasses that describe every registered action.
Plugin authors use these when implementing `register_note_types()` — PluginManager auto-creates
`ActionDefinition` instances for each `NoteTypeDefinition` returned.

::: ztlctl.actions.definitions
    options:
      show_root_heading: true
      heading_level: 3
      show_source: false
      show_signature_annotations: true

::: ztlctl.actions.registry
    options:
      show_root_heading: true
      heading_level: 3
      show_source: false
      show_signature_annotations: true
      filters:
        - "!^_"


---

# MCP Server

The MCP (Model Context Protocol) server exposes ztlctl's discovery-first tool surface to AI clients.

## Setup

```bash
# Install with MCP support
pip install ztlctl[mcp]

# Start the server
ztlctl serve --transport stdio
```

## Claude Desktop Integration

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "ztlctl": {
      "command": "ztlctl",
      "args": ["serve"]
    }
  }
}
```

## Discovery Pattern

The intended call sequence is:

1. `discover_tools` to narrow the surface by category
2. `describe_tool` to inspect a specific contract
3. `ztlctl://agent-reference` when the client needs a single onboarding payload

## Tool Categories

| Category | Tools |
|----------|-------|
| Discovery | `discover_tools`, `describe_tool`, `list_tags`, `list_source_providers` |
| Creation | `create_note`, `create_reference`, `create_task`, `create_log`, `garden_seed`, `ingest_source` |
| Lifecycle | `update_content`, `close_content`, `reweave` |
| Query | `search`, `get_document`, `get_related`, `agent_context`, `list_items`, `work_queue`, `topic_packet`, `draft_from_topic` |
| Analysis | `decision_support`, `vault_review` |
| Graph | `graph_themes`, `graph_rank`, `graph_path`, `graph_gaps`, `graph_bridges` |
| Session | `session_close`, `session_status` |

Notable additions for agent workflows:

- `ingest_source` normalizes text, files, and provider-backed URLs into notes or references
- `ingest_source` accepts a nested `source_bundle` contract for agent-fetched web and multimodal captures, while still supporting the older flat evidence fields
- ingested references persist source bundles under `sources/<reference-id>/` so later packets, drafts, and dossier exports can reuse the same evidence
- `list_source_providers` lets clients discover installed URL-ingestion providers
- `topic_packet` assembles topic-scoped learning, review, and decision bundles
- `draft_from_topic` turns a topic packet into a note, task, or decision draft without writing to the vault
- `create_note` accepts authored `body`, `key_points`, `aliases`, and `links`
- `create_reference` accepts `subtype` for `article`, `tool`, or `spec`
- `vault_review` returns a review-ready aggregate snapshot
- `session_status` lets clients reason about active-session state without inferring from errors

## Generated Client Assets

For project-local Claude and Codex setup, export generated workflow assets from the packaged templates:

```bash
ztlctl workflow export --client both
```

This writes a `.claude/` project bundle, a root `AGENTS.md`, and supporting files derived from the same portable workflow source. The exported guidance treats MCP as canonical when available and the CLI as the fallback contract.

## Available Resources

17 resources are registered by default. All URIs use the `ztlctl://` scheme.

| Resource | Description |
|----------|-------------|
| `ztlctl://self/identity` | Agent identity document |
| `ztlctl://self/methodology` | Agent methodology document |
| `ztlctl://overview` | Vault statistics and structure |
| `ztlctl://work-queue` | Prioritized task list |
| `ztlctl://review/dashboard` | External review workbench snapshot |
| `ztlctl://garden/backlog` | Enrichment backlog signals from stale seeds and orphan notes |
| `ztlctl://decision-queue` | Recent decisions plus active work queue |
| `ztlctl://capture/spec` | Source-bundle contract for agent capture and ingest handoff |
| `ztlctl://topics` | Available topic directories |
| `ztlctl://context` | Full assembled context (identity + methodology + overview) |
| `ztlctl://agent-reference` | Onboarding payload with tool guidance and workflow examples |
| `ztlctl://recipes` | Index of available agent orchestration recipes |
| `ztlctl://recipes/research-capture` | Research-capture workflow: search, create notes, link evidence |
| `ztlctl://recipes/review-triage` | Review-triage workflow: work queue, inspect, update, archive |
| `ztlctl://recipes/knowledge-synthesis` | Knowledge-synthesis workflow: search, find gaps, draft, reweave |
| `ztlctl://docs/index` | Navigation map of all ztlctl documentation pages |
| `ztlctl://docs/search` | Documentation search guidance — use `docs_search` tool for queries |

## Available Prompts

9 prompts are registered by default (source: `src/ztlctl/mcp/prompts.py`).

| Prompt | Description |
|--------|-------------|
| `research_session` | Start a structured research session |
| `knowledge_capture` | Guided knowledge capture workflow |
| `vault_orientation` | Orient to the current vault state |
| `decision_record` | Record an architectural decision |
| `topic_learn` | Gather and learn from a topic packet |
| `topic_review` | Review a topic for stale and weakly connected knowledge |
| `topic_decision` | Prepare a decision-ready topic packet and draft |
| `capture_web_source` | Turn a fetched web page into a source bundle for ingest |
| `capture_multimodal_source` | Turn extracted multimodal evidence into a source bundle for ingest |

## Example: Creating a Note via MCP

A complete create_note tool call and response:

```json
// Tool call
{
  "tool": "create_note",
  "arguments": {
    "title": "Zettelkasten Linking Principles",
    "body": "Each note should represent a single atomic idea...",
    "tags": ["zettelkasten", "methodology"],
    "maturity": "seed"
  }
}

// Response
{
  "ok": true,
  "content_id": "ztl_A3F8B2C1",
  "title": "Zettelkasten Linking Principles",
  "path": "notes/zettelkasten/ztl_A3F8B2C1.md",
  "maturity": "seed",
  "tags": ["zettelkasten", "methodology"],
  "links_added": 0
}
```

After creation, run `reweave` with the returned `content_id` to discover and add connections to related notes automatically.

## Agent Integration

For machine-readable schemas, state machines, and structured interaction flows, see the [Agent System Manual](agents.md). That page is optimized for LLM consumers and covers error recovery patterns, constraint rules, and orchestration contracts.


---

# Agent System Manual

> This page is for LLM systems consuming ztlctl via MCP or CLI. For human-readable workflow guides, see [Agentic Workflows](agentic-workflows.md).

All schemas, states, and constraints on this page are verified against source code. Use structured access patterns; do not infer behavior from names alone.

---

## System Capabilities

| Category | Capability | CLI | MCP Tool |
|----------|-----------|-----|----------|
| Discovery | List available tools by category | `ztlctl --help` | `discover_tools` |
| Discovery | Describe a specific tool contract | `ztlctl <cmd> --help` | `describe_tool` |
| Discovery | List all registered tags | `ztlctl query list --type tag` | `list_tags` |
| Discovery | List installed source providers | — | `list_source_providers` |
| Creation | Create note | `ztlctl create note` | `create_note` |
| Creation | Create reference | `ztlctl create reference` | `create_reference` |
| Creation | Create task | `ztlctl create task` | `create_task` |
| Creation | Create garden seed | `ztlctl garden seed` | `garden_seed` |
| Creation | Ingest text/file/URL source | `ztlctl ingest text\|file\|url` | `ingest_source` |
| Lifecycle | Update metadata | `ztlctl update <id>` | `update_content` |
| Lifecycle | Close/archive content | `ztlctl archive <id>` | `close_content` |
| Lifecycle | Discover and apply links | `ztlctl reweave` | `reweave` |
| Query | Full-text search | `ztlctl query search` | `search` |
| Query | Get single document | `ztlctl query get <id>` | `get_document` |
| Query | List with filters | `ztlctl query list` | `list_items` |
| Query | Prioritized work queue | `ztlctl query work-queue` | `work_queue` |
| Query | Topic learning/review/decision packet | `ztlctl query packet` | `topic_packet` |
| Query | Draft from topic packet | `ztlctl query draft` | `draft_from_topic` |
| Query | Agent context assembly | `ztlctl agent context` | `agent_context` |
| Analysis | Decision support aggregate | `ztlctl query decision-support` | `decision_support` |
| Analysis | Vault review snapshot | — | `vault_review` |
| Graph | Find related content | `ztlctl graph related <id>` | `get_related` |
| Graph | Discover topic clusters | `ztlctl graph themes` | `graph_themes` |
| Graph | PageRank-ordered nodes | `ztlctl graph rank` | `graph_rank` |
| Graph | Shortest path between nodes | `ztlctl graph path <a> <b>` | `graph_path` |
| Graph | Orphan/gap detection | `ztlctl graph gaps` | `graph_gaps` |
| Graph | Bridge node detection | `ztlctl graph bridges` | `graph_bridges` |
| Session | Start session | `ztlctl agent session start` | `session_start` |
| Session | Close session + enrichment | `ztlctl agent session close` | `session_close` |
| Session | Query session status | `ztlctl agent session status` | `session_status` |
| Session | Log reasoning entry | `ztlctl agent session log` | — |
| Export | Markdown export | `ztlctl export markdown` | — |
| Export | Graph export (dot/json) | `ztlctl export graph` | — |
| Export | Review dashboard | `ztlctl export dashboard` | — |
| Check | Vault integrity scan | `ztlctl check` | — |
| Check | Auto-fix issues | `ztlctl check --fix` | — |
| Check | Full rebuild from files | `ztlctl check --rebuild` | — |

---

## Entity Schemas

### Note

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `title` | `str` | Yes | 3–200 chars | Unique display title |
| `content_type` | `str` | Yes | `"note"` | Fixed; cannot change post-creation |
| `subtype` | `str` | No | `knowledge` \| `decision` \| `None` | Subtype affects lifecycle rules |
| `tags` | `list[str]` | No | `domain/scope` format recommended | Bare tags work but emit warning |
| `topic` | `str` | No | Must match a vault topic directory | Routing prefix for context assembly |
| `maturity` | `str` | No | `seed` \| `budding` \| `evergreen` | Garden lifecycle; human-advisory |
| `body` | `str` | No | Markdown | Content body |
| `key_points` | `list[str]` | No | — | Structured summary bullets |
| `links` | `dict[str, list[str]]` | No | Wikilink titles `[[Title]]` | Explicit outgoing link map |
| `aliases` | `list[str]` | No | — | Alternative lookup names |
| `session` | `str` | No | Must be a `LOG-NNNN` of an open session | Associates note with session |

**Computed fields (read-only):**

| Field | Type | Description |
|-------|------|-------------|
| `id` | `str` | `ztl_<8-char hash>` |
| `status` | `str` | `draft` \| `linked` \| `connected` — computed from outgoing link count |
| `created_at` | `str` | ISO 8601 timestamp |
| `modified_at` | `str` | ISO 8601 timestamp |

### Reference

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `title` | `str` | Yes | 3–200 chars | Unique display title |
| `content_type` | `str` | Yes | `"reference"` | Fixed; cannot change post-creation |
| `subtype` | `str` | No | `article` \| `tool` \| `spec` | Classification (no lifecycle enforcement) |
| `url` | `str` | No | Valid URL | Source location |
| `tags` | `list[str]` | No | `domain/scope` format recommended | — |
| `topic` | `str` | No | Must match a vault topic directory | — |
| `body` | `str` | No | Markdown | Annotation or excerpt |
| `session` | `str` | No | Must be a `LOG-NNNN` of an open session | — |

**Computed fields:**

| Field | Type | Description |
|-------|------|-------------|
| `id` | `str` | `ref_<8-char hash>` |
| `status` | `str` | `captured` \| `annotated` — computed from annotation presence |

### Task

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `title` | `str` | Yes | 3–200 chars | Unique display title |
| `content_type` | `str` | Yes | `"task"` | Fixed |
| `priority` | `str` | No | `low` \| `medium` \| `high` | Work queue scoring signal |
| `impact` | `str` | No | `low` \| `medium` \| `high` | Work queue scoring signal |
| `effort` | `str` | No | `low` \| `medium` \| `high` | Work queue scoring signal |
| `tags` | `list[str]` | No | `domain/scope` format | — |

**Computed fields:**

| Field | Type | Description |
|-------|------|-------------|
| `id` | `str` | `TASK-NNNN` (sequential) |
| `status` | `str` | `inbox` \| `active` \| `blocked` \| `done` \| `dropped` |
| `score` | `float` | `priority × impact / effort` — work queue ordering |

### Session Log

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `topic` | `str` | Yes | Free text | Session focus topic |
| `summary` | `str` | No (at close) | Free text | Close summary |

**Computed fields:**

| Field | Type | Description |
|-------|------|-------------|
| `id` | `str` | `LOG-NNNN` (sequential) |
| `status` | `str` | `open` \| `closed` |
| `created_at` | `str` | Session start timestamp |

---

## Lifecycle State Machines

All status transitions are enforced. Invalid transitions return `ServiceResult(success=False)`.

### Note Status

Status is computed from outgoing link count — not settable directly via update.

```
             (0 outgoing links)
                    |
                  DRAFT
                    |
           (>= 1 outgoing link)
                    |
                 LINKED
                    |
           (>= 3 outgoing links)
                    |
               CONNECTED
```

**Thresholds (from `domain/lifecycle.py`):**
- `draft` → `linked`: requires 1+ outgoing links
- `linked` → `connected`: requires 3+ outgoing links

### Note Decision Subtype Status

```
PROPOSED ──> ACCEPTED ──> SUPERSEDED
```

Trigger `proposed → accepted` via `ztlctl update <id> --status accepted`.
Trigger `accepted → superseded` via `ztlctl supersede <old_id> <new_id>`.

### Reference Status

```
CAPTURED ──> ANNOTATED
```

Status advances when the reference body field is populated.

### Task Status

```
        INBOX
       /     \
    ACTIVE  DROPPED
   /      \
BLOCKED   DONE
   \
  ACTIVE (return from blocked)
   \
  DROPPED
```

Valid transitions:
- `inbox → active`, `inbox → dropped`
- `active → blocked`, `active → done`, `active → dropped`
- `blocked → active`, `blocked → dropped`
- `done →` (terminal)
- `dropped →` (terminal)

### Session Log Status

```
OPEN <──> CLOSED
```

Sessions are reopenable. Only one session may be `open` at a time (enforced server-side).

### Garden Maturity

Advisory only — not enforced by the engine. Human-driven via `ztlctl update --maturity`.

```
SEED ──> BUDDING ──> EVERGREEN
```

---

## Constraint Rules

These are hard constraints enforced by service layer and domain rules:

1. **Content type is immutable.** `content_type` cannot be changed after creation.
2. **One open session at a time.** `session_start` returns an error if a session with `status=open` already exists.
3. **Session must exist before session-close.** `session_close` requires a `LOG-NNNN` with `status=open`.
4. **Note status is computed, not settable.** `update --status linked` is rejected for notes — status derives from outgoing link count.
5. **Decision note subtype status uses a separate machine.** Decision notes (`subtype=decision`) use `proposed/accepted/superseded`, not the note link-count machine.
6. **Decision notes are excluded from auto-reweave.** The Reweave plugin skips `subtype=decision` to protect decision integrity. Call `ztlctl reweave --id <id>` manually to link a decision note.
7. **Tags should use `domain/scope` format.** Bare tags (e.g., `python`) are accepted but emit a warning and cannot be filtered with domain-prefix queries.
8. **Task scoring signals are optional but influence queue ordering.** Omitting `priority`, `impact`, or `effort` reduces work queue signal quality.
9. **Batch mode requires session close to commit.** With `[plugins.git] batch_commits = true`, file changes are staged but not committed until `session_close`.
10. **`check --rebuild` reconstructs durable content only.** Session rows, event WAL, and generated `self/` files are not part of the file-first guarantee and are not rebuilt.
11. **Semantic search requires sqlite-vec extension.** `ztlctl vector search` and `[search] semantic_enabled = true` require the sqlite-vec Python package.
12. **Garden maturity `evergreen` requires 5+ key points and 3+ bidirectional links** (configurable via `[garden]` section).

---

## Deterministic Interaction Flows

### Research Capture Flow

Purpose: Capture external sources and synthesize findings into linked notes.

```
Step 1: Start session
  CLI: ztlctl agent session start "Research: {topic}" --json
  MCP: session_start(topic="{topic}")
  → Returns: {"id": "LOG-NNNN", "status": "open"}

Step 2: Orient to existing knowledge
  CLI: ztlctl agent context --topic "{topic}" --budget 4000 --json
  MCP: agent_context(topic="{topic}", budget=4000)
  → Returns: 5-layer context payload

Step 3: Ingest source material
  CLI: ztlctl ingest text "{title}" --stdin --as reference --tags "{tag}" --json
  MCP: ingest_source(title="{title}", content="{text}", as_type="reference", tags=["{tag}"])
  → Returns: {"id": "ref_XXXXXXXX", "status": "captured"}

Step 4: Create synthesis note
  CLI: ztlctl create note "{title}" --tags "{tag}" --session LOG-NNNN --json
  MCP: create_note(title="{title}", tags=["{tag}"], session="LOG-NNNN")
  → Returns: {"id": "ztl_XXXXXXXX", "status": "draft", "reweave_suggestions": [...]}
  Note: Reweave plugin fires automatically for notes/references

Step 5: Log key insights
  CLI: ztlctl agent session log "{insight}" --pin --json
  MCP: (no direct MCP equivalent — use CLI or session log entry via create_log)

Step 6: Close session
  CLI: ztlctl agent session close --summary "{summary}" --json
  MCP: session_close(summary="{summary}")
  → Returns: {"reweave_count": N, "orphan_count": N, "integrity_issues": N}
```

### Knowledge Retrieval Flow

Purpose: Retrieve structured context for a topic before reasoning or creating.

```
Step 1: Search existing content
  CLI: ztlctl query search "{query}" --rank-by relevance --json
  MCP: search(query="{query}", rank_by="relevance", limit=10)
  → Returns: list of matching items with scores

Step 2 (optional): Get topic packet for richer context
  CLI: ztlctl query packet --topic "{topic}" --mode learn --json
  MCP: topic_packet(topic="{topic}", mode="learn")
  → Returns: notes, references, decisions, graph neighbors, bridge candidates

Step 3 (optional): Get graph neighbors
  CLI: ztlctl graph related {id} --depth 2 --top 10 --json
  MCP: get_related(content_id="{id}", depth=2, top=10)
  → Returns: spread-activated related items with distance scores

Step 4 (optional): Draft from topic packet
  CLI: ztlctl query draft --topic "{topic}" --target note --json
  MCP: draft_from_topic(topic="{topic}", target="note")
  → Returns: draft payload (does NOT write to vault — caller decides to create)
```

### Session Management Flow

Purpose: Start, track, and close a bounded work session.

```
Step 1: Check session status before starting
  CLI: ztlctl agent session status --json
  MCP: session_status()
  → Returns: {"active": false} or {"active": true, "session_id": "LOG-NNNN"}

Step 2: Start session (only if no active session)
  CLI: ztlctl agent session start "{topic}" --json
  MCP: session_start(topic="{topic}")
  Constraint: Fails if a session is already open

Step 3: Do work (create notes/references, log reasoning)

Step 4: Check token budget
  CLI: ztlctl agent session cost --report {token_budget} --json
  → Returns: token usage per layer, pressure status

Step 5: Close session
  CLI: ztlctl agent session close --summary "{summary}" --json
  MCP: session_close(summary="{summary}")
  → Triggers: reweave → orphan sweep → integrity check → graph materialization
```

---

## Input/Output Schemas

### Create Note — Request

```json
{
  "title": "string (required, 3-200 chars)",
  "content_type": "note",
  "subtype": "knowledge | decision | null",
  "tags": ["domain/scope"],
  "topic": "string (vault topic directory name)",
  "maturity": "seed | budding | evergreen",
  "body": "string (markdown)",
  "key_points": ["string"],
  "links": {"outgoing": ["[[Target Title]]"]},
  "aliases": ["string"],
  "session": "LOG-NNNN"
}
```

### Create Note — Response

```json
{
  "success": true,
  "data": {
    "id": "ztl_a1b2c3d4",
    "title": "string",
    "content_type": "note",
    "status": "draft",
    "created_at": "2026-01-01T00:00:00Z"
  },
  "meta": {
    "reweave_suggestions": [
      {"id": "ztl_e5f6g7h8", "title": "string", "score": 0.74}
    ]
  }
}
```

### Search — Request

```json
{
  "query": "string (required)",
  "rank_by": "relevance | recency | graph | review | garden",
  "content_type": "note | reference | task | log | null",
  "limit": 10,
  "tags": ["domain/scope"]
}
```

### Search — Response

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "ztl_a1b2c3d4",
        "title": "string",
        "content_type": "note",
        "status": "linked",
        "score": 0.87,
        "tags": ["lang/python"]
      }
    ],
    "total": 42
  }
}
```

### Session Close — Response

```json
{
  "success": true,
  "data": {
    "session_id": "LOG-NNNN",
    "status": "closed",
    "reweave_count": 7,
    "orphan_count": 2,
    "integrity_issues": 0
  }
}
```

### ServiceResult Envelope (all operations)

```json
{
  "success": true | false,
  "data": {},
  "error": {
    "message": "string",
    "code": "string",
    "recovery": "string — corrective action hint"
  },
  "meta": {}
}
```

`error` is present only when `success` is `false`. `meta` contains operation-specific context (e.g., reweave suggestions, telemetry spans). Always check `success` before consuming `data`.

---

## Error Handling

| Error Condition | CLI Exit Code | `error.message` Pattern | `error.recovery` Hint |
|----------------|--------------|------------------------|----------------------|
| Vault not initialized | 1 | `"Vault not initialized"` | `"Run ztlctl init"` |
| Note/reference not found | 1 | `"Content {id} not found"` | `"Verify ID format: ztl_/ref_/TASK-/LOG-"` |
| Session already open | 1 | `"Session already open"` | `"Close existing session first"` |
| No active session | 1 | `"No open session"` | `"Run ztlctl agent session start"` |
| Invalid content type | 1 | `"Invalid content type"` | `"Use: note, reference, task"` |
| Invalid status transition | 1 | `"Invalid transition"` | `"Check allowed transitions for content type"` |
| Title too short | 1 | `"Title must be 3+ chars"` | `"Provide a longer title"` |
| Duplicate title | 1 | `"Title already exists"` | `"Use a unique title or update existing"` |
| Semantic search unavailable | 1 | `"sqlite-vec not installed"` | `"pip install sqlite-vec"` |
| Config not found | 1 | `"ztlctl.toml not found"` | `"Run ztlctl init or cd to vault root"` |

**Retry guidance:**

- Exit code 0: Success — proceed.
- Exit code 1, `recovery` present: Apply recovery hint, then retry once.
- Exit code 1, no `recovery`: Structural problem — do not retry without human input.
- Exit code 2: CLI usage error (bad flag/argument) — fix the call, do not retry as-is.

---

## MCP Discovery Protocol

The recommended onboarding sequence for any MCP client:

```
1. discover_tools(category="all")
   → Returns categorized tool list with one-line descriptions

2. describe_tool(name="{tool_name}")
   → Returns full parameter schema, examples, and return contract

3. ztlctl://agent-reference   (read resource)
   → Returns onboarding payload: tool guidance, workflow examples, vault orientation
```

For ongoing operation, use read resources to avoid tool calls for static context:

| Resource URI | Content |
|-------------|---------|
| `ztlctl://self/identity` | Vault identity and agent personality |
| `ztlctl://self/methodology` | Vault methodology and workflow guidance |
| `ztlctl://overview` | Vault statistics and content counts |
| `ztlctl://work-queue` | Prioritized task list |
| `ztlctl://decision-queue` | Recent decisions + active work queue |
| `ztlctl://garden/backlog` | Stale seeds and orphan notes |
| `ztlctl://capture/spec` | Source bundle contract for ingest handoff |
| `ztlctl://agent-reference` | Full agent onboarding payload |

See [MCP Server](mcp.md) for the complete tool catalog, resource list, and prompt library.


---
