Skip to content

Epics & Stories

Work organized into epics and user stories, with FR/NFR traceability.

Status: Draft v0.1 · 2026-05-03 Source documents: PRD.md, SPEC.md, API.md, CLI.md, MCP.md, AI.md Companions: FR.md, NFR.md

This document organizes v1 work into epics and user stories. Stories are written from the user’s point of view; agents are treated as users (per PRD §3). Every story links back to FR / NFR IDs so engineering and QA can trace coverage.


IDPersonaNotes
P1Agent operator (primary)Power user driving Claude Code, Cursor, an autonomous agent, or a custom workflow. Cares about determinism, idempotency, structured outputs.
P2Self-hosterPrivacy-focused; runs the container themselves. Cares about footprint, no telemetry, single docker run.
P3Existing Obsidian userHas years of .md files. Must not be forced to migrate; must coexist with Obsidian.
P4Library authorScripts vaults from Python/Go/Rust. Cares about a stable HTTP API and OpenAPI.
P5The agent itselfA first-class user, not an integration. Driven by the agent operator (P1).
[E#-S#] As <persona>, I want <capability>, so that <outcome>.
Acceptance:
- <observable behavior>
- <observable behavior>
FR: FR-<id>, FR-<id>
NFR: NFR-<id>
Trace: <PRD §x | journey Jn>

Priority on each story: P0 (release-blocking for v1), P1 (target for v1), P2 (post-v1).


E1 — Vault foundation & on-disk compatibility

Section titled “E1 — Vault foundation & on-disk compatibility”

Goal: Open any existing Obsidian vault unchanged; coexist safely.

StoryPriorityDetail
[E1-S1]P0As P3, I want indx to open my existing vault folder without any migration, so that I do not lose work or layout. Acceptance: point indx at a 5-year-old vault; UI loads the file tree; no files in the vault are modified. FR: FR-V-1, FR-V-2. NFR: NFR-COMPAT-3, NFR-COMPAT-4. Trace: J6.
[E1-S2]P0As P3, I want indx to never write to .obsidian/, so that I can quit indx and reopen Obsidian on the same folder with no surprises. Acceptance: checksum of .obsidian/ unchanged after a session. FR: FR-V-3. NFR: NFR-COMPAT-3. Trace: J6.
[E1-S3]P0As P2, I want .indx/ to be safely deletable and recreatable, so that I can confidently move or back up vaults. Acceptance: rm -rf .indx/; restart container; vault_status reports the same indexed_notes count after one reindex. FR: FR-V-2, FR-V-4. NFR: NFR-REL-4.
[E1-S4]P1As P2, I want to relocate .indx/ outside the vault, so that my Git remotes don’t carry index data. Acceptance: setting INDX_INDEX_DIR=/data produces an empty .indx/ in the vault and a populated index in /data. FR: FR-V-6. Trace: PRD §11 Q1.
[E1-S5]P0As P5, I want every path to be vault-relative POSIX with no .. escapes, so that I can never accidentally write outside the vault. Acceptance: path traversal corpus all rejected with 400 bad_request. FR: FR-V-5. NFR: NFR-SEC-1.

Goal: Full markdown CRUD with atomic writes, structured patches, optimistic concurrency.

StoryPriorityDetail
[E2-S1]P0As P5, I want to read a note and receive { etag, frontmatter, outline, body } in one call, so that I do not need to parse the file myself. Acceptance: note_read returns all four; include selector trims fields. FR: FR-N-1, FR-N-9.
[E2-S2]P0As P5, I want to create or replace a note in one call and get the new resource back, so that I do not need a follow-up read. Acceptance: PUT /v1/notes/{p} returns the full new resource with new ETag. FR: FR-N-2, FR-N-3, FR-E-8.
[E2-S3]P0As P5, I want to apply small AST-aware patches (e.g. “insert under ## Today”), so that I do not ship whole files for trivial edits. Acceptance: all nine patch ops execute against the AST and re-serialize without reformatting unrelated content. FR: FR-E-4, FR-E-5, FR-E-6. Trace: J1.
[E2-S4]P0As P5, I want optimistic-concurrency on every write, so that two agents editing the same note do not silently overwrite each other. Acceptance: If-Match mismatch returns 409 etag_mismatch with current_etag. FR: FR-E-3. NFR: NFR-REL-2.
[E2-S5]P0As P5, I want safe retries via Idempotency-Key, so that network glitches do not corrupt the vault. Acceptance: repeated key returns cached response with X-Indx-Idempotent-Replay: true; same key + different body → 409 idempotency_key_reused. FR: FR-E-7. NFR: NFR-REL-5.
[E2-S6]P0As P5, I want to delete a note and have a 5-minute tombstone, so that I can distinguish “deleted” from “never existed”. Acceptance: subsequent reads return 410 gone for 5 min, then 404. FR: FR-N-4.
[E2-S7]P0As P1, I want moving a note to rewrite incoming wikilinks by default, so that the link graph stays intact. Acceptance: move returns { from, to, rewrites }; update_links: false skips rewriting. FR: FR-N-5, FR-L-4. Trace: J3.
[E2-S8]P0As P5, I want every write to be atomic (write-temp + rename) and the index updated synchronously, so that crashes never leave a partial file. Acceptance: SIGKILL between write and rename leaves the old file intact. FR: FR-E-1, FR-E-2. NFR: NFR-REL-1.
[E2-S9]P1As P1, I want a --dry-run for every write verb, so that I can preview the change without touching disk. Acceptance: --dry-run returns the would-be resource and skips the write/index update. FR: FR-CLI-5.

Goal: Lexical-by-default; semantic and hybrid when configured; structural filters everywhere.

StoryPriorityDetail
[E3-S1]P0As P5, I want lexical full-text search out of the box (no AI key required), so that the product is useful with zero configuration. Acceptance: vault_search { q, mode: "lexical" } returns ranked results in p95 ≤ 200 ms on 10 k notes. FR: FR-S-1. NFR: NFR-PERF-3. Trace: J2.
[E3-S2]P1As P5, I want hybrid search (lexical + semantic via RRF) when embeddings are configured, so that I get better recall on conceptual queries. Acceptance: mode: "hybrid" returns mode_used: "hybrid" with embeddings on; falls back to lexical with embeddings_unavailable warning when off. FR: FR-S-3, FR-S-4.
[E3-S3]P0As P5, I want structural filters (tag, path, frontmatter, linksTo, linkedFrom) composable with any mode, so that I can narrow results deterministically. Acceptance: filter combinations return only matching docs. FR: FR-S-5.
[E3-S4]P0As P5, I want each result to include { path, title, score, snippet, tags, matched_in }, so that I can rank without re-reading. FR: FR-S-6.
[E3-S5]P0As P5, I want to stream large result sets as NDJSON, so that I can pipeline downstream tools. Acceptance: Accept: application/x-ndjson returns one row per line, no envelope. FR: FR-S-7, FR-N-7. NFR: NFR-PERF-6.
[E3-S6]P0As P3, I want a search box in the UI that uses the same search engine as agents, so that results match across surfaces. FR: FR-UI-2, FR-UI-4.

Goal: First-class graph operations and tag operations.

StoryPriorityDetail
[E4-S1]P0As P5, I want to fetch backlinks and forward links for any note, so that I can reason about connectivity. FR: FR-L-1. Trace: J3.
[E4-S2]P0As P5, I want to list unresolved wikilinks, so that I can repair broken links. FR: FR-L-2.
[E4-S3]P0As P5, I want to list orphan notes, so that I can decide which to link, archive, or delete. FR: FR-L-3. Trace: J3.
[E4-S4]P0As P5, I want to list all tags with counts and notes for a tag, so that I can browse by tag. FR: FR-T-1, FR-T-2.
[E4-S5]P0As P5, I want to bulk-rename a tag with a dry_run option, so that I can refactor my taxonomy safely. FR: FR-T-3.
[E4-S6]P1As P3, I want a basic graph view in the UI, so that I can see clusters at a glance. FR: FR-UI-6, FR-L-6.

Goal: JSON Canvas read/write parity sufficient for agent edits; visual viewer in v1, visual editor post-v1.

StoryPriorityDetail
[E5-S1]P0As P5, I want to read and write .canvas files conforming to JSON Canvas 1.0, so that I can manipulate canvases without screen-scraping. FR: FR-C-1.
[E5-S2]P0As P5, I want canvas patch ops (add_node, update_node, remove_node, add_edge, remove_edge, move_node), so that I can edit canvases incrementally. FR: FR-C-2.
[E5-S3]P0As P5, I want unknown canvas fields preserved on round-trip, so that future spec extensions don’t get stripped. FR: FR-C-3. NFR: NFR-COMPAT-2.
[E5-S4]P1As P3, I want a read-only canvas viewer in the UI, so that I can inspect canvases without leaving the app. FR: FR-C-4, FR-UI-6.
[E5-S5]P2As P3, I want a visual canvas editor in the UI. (post-v1) FR: FR-C-4.

Goal: Read and query .base databases; minimal write support.

StoryPriorityDetail
[E6-S1]P0As P5, I want to read .base YAML and execute its query against the vault, so that I get rows without owning a query engine. FR: FR-B-1, FR-B-2.
[E6-S2]P1As P5, I want NDJSON streaming for large base queries, so that I can paginate or pipeline. FR: FR-B-2.
[E6-S3]P1As P5, I want to create or update a .base YAML directly via API, so that I can templatize databases. FR: FR-B-3.
[E6-S4]P2As P3, I want a visual base builder. (post-v1) FR: FR-B-4.

Goal: Resource-oriented, schema-first, agent-friendly.

StoryPriorityDetail
[E7-S1]P0As P4, I want every response wrapped in { ok, data | error } with stable error codes, so that my client library can branch deterministically. FR: FR-API-2, FR-API-3. NFR: NFR-USE-AGENT-4.
[E7-S2]P0As P4, I want a complete OpenAPI 3.1 document at /openapi.json reflecting the running server, so that I can codegen clients. FR: FR-API-7. NFR: NFR-COMPAT-API-4.
[E7-S3]P0As P5, I want cursor-based pagination on every list/search endpoint, so that I never hit a window-shifted result set. FR: FR-API-5.
[E7-S4]P0As P5, I want content negotiation (json / ndjson / text/event-stream) so that I can pick the right shape for the job. FR: FR-API-10, FR-W-3.
[E7-S5]P0As P4, I want /v1/* to be additive-only, so that I never need to re-test my client after an indx upgrade. FR: FR-API-8. NFR: NFR-COMPAT-API-1, NFR-COMPAT-API-2.
[E7-S6]P0As P2, I want per-token rate limits with standard 429 headers, so that I can throttle agents and protect the box. FR: FR-API-9. NFR: NFR-SEC-4.

Goal: Deterministic, structured, atomic, scriptable.

StoryPriorityDetail
[E8-S1]P0As P5, I want JSON-by-default stdout, no spinners, no ANSI, so that I can pipe and diff outputs reliably. FR: FR-CLI-3. NFR: NFR-USE-AGENT-3.
[E8-S2]P0As P5, I want predictable exit codes (3=not_found, 4=conflict, …), so that I can branch on failure without parsing English. FR: FR-CLI-6.
[E8-S3]P0As P5, I want every write verb to return the full new resource (read-after-write), so that I never need a follow-up GET. FR: FR-CLI-8, FR-E-8.
[E8-S4]P0As P2, I want --local mode that operates against a vault directory without a server, so that I can script offline maintenance. FR: FR-CLI-2. Trace: PRD §11 Q3.
[E8-S5]P0As P5, I want indx schema openapi/mcp/patch-ops/events so that I can introspect the live server. FR: FR-CLI-9. NFR: NFR-USE-AGENT-5.
[E8-S6]P0As P5, I want destructive verbs to require --yes and otherwise exit 4, so that retries never delete data by accident. FR: FR-CLI-7.

Goal: First-class tool catalog + resources + prompts; stdio and HTTP/SSE transports.

StoryPriorityDetail
[E9-S1]P0As P5, I want a stdio MCP server so that Claude Code, Cursor, and similar local agents can use indx natively. FR: FR-MCP-1.
[E9-S2]P0As P5, I want an HTTP/SSE MCP transport at POST /mcp, so that remote agents can connect with a token. FR: FR-MCP-1.
[E9-S3]P0As P5, I want a self-describing tool catalog with JSON Schemas derived from the same Zod tree as the API, so that schema drift is impossible. FR: FR-MCP-3, FR-PKG-1. NFR: NFR-MAINT-2.
[E9-S4]P0As P5, I want the catalog filtered by my token’s scope, so that a vault:read session does not even see write tools. FR: FR-A-4. NFR: NFR-USE-AGENT-4.
[E9-S5]P0As P5, I want every tool result to include both structuredContent and a short deterministic text summary, so that I can either parse or display. FR: FR-MCP-4.
[E9-S6]P0As P5, I want resources under vault://, including vault://search?… and vault://graph/…, so that I can browse, paginate, and subscribe — not only call tools. FR: FR-MCP-7.
[E9-S7]P0As P5, I want resources/subscribe over a glob (e.g., vault://Daily/**), so that I can react to changes without polling. FR: FR-W-6, FR-MCP-7. Trace: MCP §8.3.
[E9-S8]P1As P5, I want shipped prompts (vault_summary, daily_note, link_orphans, …), so that I am productive on first connection. FR: FR-MCP-8.
[E9-S9]P0As P5, I want every MCP tool to map 1:1 to an API endpoint and a CLI verb, so that I can debug behavior across surfaces. FR: FR-MCP-9.

Goal: Thin humane UI on top of the API; no UI-only side channels.

StoryPriorityDetail
[E10-S1]P0As P3, I want a file tree, markdown editor, and search in the UI, so that I can do basic editing without an agent. FR: FR-UI-1, FR-UI-2.
[E10-S2]P0As P3, I want a “recent changes” panel that shows who (ui / api / cli / mcp / fs) made each change, so that I can audit my agents. FR: FR-UI-3, FR-W-2. Trace: J4.
[E10-S3]P0As P3, I want every UI write to go through the same code path as API/CLI/MCP, so that there is no hidden behavior. FR: FR-UI-4. Trace: PRD §9.
[E10-S4]P1As P3, I want responsive layout down to mobile-class viewports, so that I can read on my phone. FR: FR-UI-5. NFR: NFR-USE-HUMAN-2.
[E10-S5]P1As P3, I want WCAG 2.1 AA accessibility on the editor and panels. NFR: NFR-USE-HUMAN-1.
[E10-S6]P2As P3, I want a diff/rollback view for agent-made changes. (planned for v1.1) FR: FR-UI-7.

Goal: Single-tenant token auth with scopes; documented threat model.

StoryPriorityDetail
[E11-S1]P0As P2, I want to set a single bearer token via env var to gate the whole server. FR: FR-A-1.
[E11-S2]P0As P2, I want to issue multiple tokens with scopes (vault:read, vault:write, vault:admin, path:<glob>), so that I can run least-privilege agents. FR: FR-A-1, FR-A-2.
[E11-S3]P0As P2, I want clear 401 vs 403 semantics so I can debug agent failures. FR: FR-A-3.
[E11-S4]P0As P2, I want CSRF-protected, SameSite=Strict cookies for the UI, so that a malicious site cannot drive my vault. FR: FR-A-5. NFR: NFR-SEC-5.
[E11-S5]P0As P2, I want INDX_READONLY=true to hard-block all writes, so that I can expose a public read-only vault. FR: FR-E-10.
[E11-S6]P0As P2, I want a documented threat model (docs/SECURITY.md), so that I understand what indx defends against. NFR: NFR-SEC-7.
[E11-S7]P2As P2, I want OIDC and per-user vaults. (post-v1) FR: FR-A-6.

Goal: One change stream feeding UI, audit log, and agent subscriptions.

StoryPriorityDetail
[E12-S1]P0As P5, I want SSE at GET /v1/events with filters (paths, kinds) and Last-Event-ID resume, so that long-running agents survive disconnects. FR: FR-W-3.
[E12-S2]P0As P3, I want chokidar-based watching with content hashing, so that external edits (Obsidian, git pull, sed) reindex within one debounce window. FR: FR-W-1. NFR: NFR-REL-3.
[E12-S3]P0As P2, I want a rolling NDJSON event log in .indx/events.log, so that I can audit agent activity. FR: FR-W-4. NFR: NFR-OBS-4.
[E12-S4]P0As P5, I want every event to carry an Actor, so that I can attribute changes. FR: FR-W-2.

Goal: One container, one command, sub-2s cold start, sub-100MB image.

StoryPriorityDetail
[E13-S1]P0As P2, I want one docker run to bring up indx with my vault and a token, so that setup is sub-60-seconds. FR: FR-V-1, FR-CONF-1. NFR: NFR-DEPLOY-1, NFR-USE-HUMAN-3. Trace: J5.
[E13-S2]P0As P2, I want a healthcheck endpoint suitable for Docker / Kubernetes / Vercel probes. FR: FR-OBS-2. NFR: NFR-DEPLOY-2.
[E13-S3]P0As P2, I want a docker image under 100 MB compressed, so that pulls and registry costs stay low. NFR: NFR-RES-1.
[E13-S4]P0As P2, I want idle RAM under 256 MB, so that indx coexists with my other services. NFR: NFR-RES-2.
[E13-S5]P1As P2, I want multi-arch images (amd64 + arm64), so that I can run on a Raspberry Pi or Apple Silicon. NFR: NFR-DEPLOY-4.
[E13-S6]P0As P2, I want the container to run as a non-root user, so that a compromised process cannot escalate. NFR: NFR-DEPLOY-3, NFR-SEC-6.
[E13-S7]P0As P2, I want zero outbound calls unless I configure embeddings, so that I can deploy in air-gapped environments. FR: FR-CONF-5. NFR: NFR-PRIV-1, NFR-SEC-3.
[E13-S8]P1As P2, I want optional OpenTelemetry export, so that I can plug indx into my existing observability stack. FR: FR-OBS-4. NFR: NFR-OBS-3.

Goal: Single Zod tree as the source of truth; everything else derived.

StoryPriorityDetail
[E14-S1]P0As P4, I want TypeScript types, JSON Schema, and OpenAPI 3.1 derived from one Zod tree, so that the contract cannot drift between surfaces. FR: FR-PKG-1. NFR: NFR-MAINT-2.
[E14-S2]P0As an engineer, I want adding a capability to be one PR touching schema + core + API + CLI + MCP, so that surfaces never lag. FR: FR-PKG-2. NFR: NFR-MAINT-5.
[E14-S3]P0As P5, I want runtime schema discovery via /openapi.json, tools/list, and indx schema *, so that a fresh agent is productive in one round trip. FR: FR-PKG-3. NFR: NFR-USE-AGENT-5.

E15 — Embeddings & AI integration (optional)

Section titled “E15 — Embeddings & AI integration (optional)”

Goal: Pluggable embeddings with safe defaults; product works without any AI key.

StoryPriorityDetail
[E15-S1]P0As P2, I want indx to function fully without an embeddings provider, with semantic search degraded silently, so that I can adopt incrementally. FR: FR-CONF-4, FR-S-4. NFR: NFR-USE-HUMAN-4.
[E15-S2]P1As P2, I want to point indx at Vercel AI Gateway, an OpenAI-compatible endpoint, or local Ollama, so that I keep provider choice open. FR: FR-CONF-4.
[E15-S3]P1As P2, I want allow/deny globs for paths eligible for embedding, so that I can keep sensitive notes off remote providers. NFR: NFR-PRIV-3.
[E15-S4]P0As P2, I want embedding requests to be the only outbound calls indx ever makes, so that no telemetry leaks. FR: FR-CONF-5. NFR: NFR-PRIV-1.

Goal: Ship seven built-in AI ops — summarize, ask, toc, relate, tag, metadata, extract — equally available from API, CLI, and MCP, opt-in by provider config, with verifiable citations and the same atomic-write/idempotency invariants as the rest of the system.

StoryPriorityDetail
[E17-S1]P0As P5, I want a single AI runtime with summarize, ask, toc, relate reachable identically from API/CLI/MCP, so that I never need to wire my own retrieval pipeline. Acceptance: each op is callable via POST /v1/ai/*, indx ai *, and the ai_* MCP tool; the surfaces share the same Zod schema. FR: FR-AI-1, FR-AI-3, FR-PKG-2. NFR: NFR-USE-AGENT-2. Trace: AI §1, §10.
[E17-S2]P0As P2, I want indx to be fully usable without any AI provider, so that I can run air-gapped without functional regressions. Acceptance: with no provider env var, AI tools are absent from /openapi.json and the MCP catalog; /v1/ai/* returns 503 ai_unavailable; zero outbound calls observed. FR: FR-AI-2. NFR: NFR-AI-1, NFR-PRIV-1. Trace: AI §2.
[E17-S3]P0As P2, I want chat-model providers pluggable across vercel-ai-gateway, OpenAI-compatible, and Ollama with sensible defaults inheriting from embeddings, so that I configure once and forget. Acceptance: INDX_AI_PROVIDER falls back to INDX_EMBEDDINGS_PROVIDER; switching providers requires no code change. FR: FR-AI-3, FR-CONF-4. Trace: AI §3.
[E17-S4]P0As P5, I want every AI output to carry verifiable citations ({path, etag, anchor?, line?}), so that I can re-fetch and detect drift. Acceptance: post-hoc verification drops invalid citations with citation_invalid / citation_drift; un-grounded results fail with ai_grounding_failed when cite: true. FR: FR-AI-10, FR-AI-11. NFR: NFR-AI-5. Trace: AI §6.
[E17-S5]P0As P5, I want ai_summarize over a single note, a glob, a tag, or a query, so that I can roll up arbitrary scopes in one call. Acceptance: map-reduce kicks in when scope exceeds prompt budget; output includes tldr, key_points, open_questions, outline per include[]. FR: FR-AI-4, FR-AI-5. NFR: NFR-AI-2. Trace: AI §5.1.
[E17-S6]P0As P5, I want ai_ask to retrieve via the existing search infra and answer only from retrieved evidence, so that hallucinated answers never ship. Acceptance: un-grounded answers come back with confidence: "low" and unsupported_question warning; default mode hybrid falls back to lexical with embeddings_unavailable warning. FR: FR-AI-6, FR-S-4. NFR: NFR-AI-2. Trace: AI §5.2.
[E17-S7]P0As P5, I want ai_toc for a single note that uses the indexed outline (no LLM cost), so that the common case is free. Acceptance: with include_descriptions: false, mode: "note" does not call the chat provider; usage.cost_usd === 0. FR: FR-AI-7. Trace: AI §5.3.
[E17-S8]P0As P5, I want ai_toc --moc --write <path> to materialize a Map-of-Content via the standard atomic write pipeline, so that there is no AI side-channel. Acceptance: write goes through core.vault.writeNote; honors if_match / if_not_exists; emits note.created / note.updated; response includes written: { path, etag }. FR: FR-AI-8, FR-E-1, FR-E-8. Trace: AI §5.3.
[E17-S9]P0As P5, I want ai_relate to surface related notes and (optionally) typed relations, so that I can grow the link graph deliberately. Acceptance: propose_links: true returns draft patches only — no auto-mutation. FR: FR-AI-9. NFR: NFR-AI-10. Trace: AI §5.4.
[E17-S10]P0As P5, I want streaming for long AI ops on every surface, so that latency-sensitive UIs feel responsive. Acceptance: SSE on API; NDJSON deltas on CLI (--stream); MCP notifications/progress. Providers without streaming downgrade with stream_unsupported warning, no error. FR: FR-AI-12. Trace: AI §7.
[E17-S11]P0As P5, I want a content-addressed AI cache keyed by inputs + cited paths’ etags, so that retries are free and edits invalidate only relevant entries. Acceptance: cache hit returns usage zeros and ai_cache_hit warning; editing a single note invalidates only entries whose scope referenced it. FR: FR-AI-13. NFR: NFR-AI-3, NFR-AI-4. Trace: AI §8.1.
[E17-S12]P0As P5, I want Idempotency-Key to apply to AI ops too, so that retried POST /v1/ai/* calls don’t re-charge generation. Acceptance: same key + same body returns cached response with X-Indx-Idempotent-Replay: true; same key + different body → 409 idempotency_key_reused. FR: FR-AI-14, FR-E-7. Trace: AI §8.2.
[E17-S13]P1As P2, I want a daily AI cost ceiling, so that runaway agents can’t bankrupt me. Acceptance: over-cap calls return 429 ai_quota_exceeded with Retry-After to UTC midnight without contacting the provider. FR: FR-AI-15. NFR: NFR-AI-8. Trace: AI §8.3.
[E17-S14]P0As P2, I want INDX_AI_ALLOW_GLOBS / INDX_AI_DENY_GLOBS, so that sensitive paths never leave the vault. Acceptance: deny-listed paths are excluded from AI scopes regardless of caller input; excluded items reported via globs_excluded. FR: FR-AI-4. NFR: NFR-AI-9, NFR-PRIV-3. Trace: AI §14.
[E17-S15]P0As P3, I want every AI invocation logged as a metadata-only audit event, so that I can see who summarized what without leaking content. Acceptance: ai.invocation events appear on /v1/events and in .indx/events.log; payload contains paths/counts/costs only — no prompts or outputs. FR: FR-AI-18. NFR: NFR-AI-7, NFR-PRIV-2. Trace: AI §12.
[E17-S16]P0As P5, I want ai_status for a one-call discovery handshake, so that I learn the AI capabilities of a strange indx server in one round trip. Acceptance: GET /v1/ai/status (and equivalents) returns {enabled, provider, model, embeddings, cache, spend_today_usd} without ever calling the provider. FR: FR-AI-19. NFR: NFR-USE-AGENT-5. Trace: AI §10.
[E17-S17]P1As P5, I want ai://summary/<scope> and ai://moc/<scope> MCP resources I can subscribe to, so that I can keep a derived view fresh without polling. Acceptance: subscribed scope content changes fire notifications/resources/updated; the agent re-reads to get an updated summary/MOC. FR: FR-AI-20, FR-W-6. Trace: AI §10.3.
[E17-S18]P0As an engineer, I want AI ops to be deterministic in shape (and recordable for tests), so that adapter tests don’t depend on a live provider. Acceptance: with temperature: 0, seed: 0, and the recording stub, AI ops produce byte-identical structured payloads in CI. NFR: NFR-AI-6. Trace: AI §13.
[E17-S19]P0As P5, I want ai_tag to suggest tags biased toward the existing vault vocabulary, so that taxonomies don’t drift. Acceptance: with vocabulary.use_existing: true, returned tags prefer existing ones; is_new flags new entries; min_confidence and max_per_note are honored. FR: FR-AI-21. NFR: NFR-AI-12. Trace: AI §5.5.
[E17-S20]P0As P5, I want ai_tag --apply --apply-mode merge to write through the standard frontmatter patch path, so that I never get a partial mid-note write or drift between FS and index. Acceptance: each note’s tags are dedup-unioned via core.notes.patch (atomic per note); the response includes new etag per path. FR: FR-AI-22, FR-AI-30. NFR: NFR-AI-13. Trace: AI §5.5, §5.8.
[E17-S21]P0As P5, I want ai_metadata to fill typed frontmatter fields with JSON Schema validation, so that I never persist garbage values. Acceptance: model output is constrained by fields[]; post-hoc validation drops invalids with metadata_invalid; required field absences surface as metadata_missing (still op success). FR: FR-AI-23. Trace: AI §5.6.
[E17-S22]P0As P3, I want ai_metadata --apply --apply-mode set_missing, so that AI fills only what I haven’t curated. Acceptance: existing scalar values are preserved; merge only union-extends list values; writes round-trip byte-identical through Obsidian. FR: FR-AI-24. NFR: NFR-AI-14, NFR-COMPAT-1. Trace: AI §5.6.
[E17-S23]P0As P5, I want ai_extract to take any JSON Schema and return validated structured records, so that I can pipe vault data into Bases or downstream tools. Acceptance: invalid input schema → 422 ai_schema_invalid; per-record validation drops with extract_invalid; destination: "frontmatter" requires apply: true + destination_key to write. FR: FR-AI-25, FR-AI-26. Trace: AI §5.7.
[E17-S24]P0As P5, I want batch apply ops (tag/metadata/extract with apply: true) to be per-note atomic with per-path if_match, so that concurrent edits never silently overwrite my work. Acceptance: mid-batch SIGKILL leaves a deterministic prefix written; per-path mismatches return 409 ai_apply_conflict with failed[] listing path/expected/actual; standard note.updated events fire per write plus one roll-up ai.invocation. FR: FR-AI-27, FR-AI-28. NFR: NFR-AI-13. Trace: AI §5.8.
[E17-S25]P1As P2, I want apply ops to be safely retryable, so that a network blip on a 200-note batch doesn’t double-apply. Acceptance: repeating an apply after success is a no-op per path (apply_skipped_no_change); Idempotency-Key short-circuits the entire request to the cached response within 24 h. FR: FR-AI-29, FR-E-7. Trace: AI §5.8, §8.2.

Goal: Gated, automatable release process backed by benchmarks.

StoryPriorityDetail
[E16-S1]P0As an engineer, I want a 50-task agent benchmark gating each release at ≥ 95 % success, so that regressions in agent UX are caught. NFR: NFR-USE-AGENT-1.
[E16-S2]P0As an engineer, I want round-trip property tests against a 1 000-vault corpus, so that compatibility regressions are caught. NFR: NFR-COMPAT-1, NFR-COMPAT-2.
[E16-S3]P0As an engineer, I want performance gates (cold start, read p95, search p95, reindex) running in CI, so that perf regressions block merges. NFR: NFR-PERF-1 to NFR-PERF-5.
[E16-S4]P0As an engineer, I want a security gate (dep audit + path-traversal corpus + ZAP probe), so that we ship with no known-HIGH CVEs. NFR: NFR-SEC-1, NFR-SEC-5, NFR-SEC-8.
[E16-S5]P0As an engineer, I want an OpenAPI breaking-change diff in CI, so that /v1 stability is enforced mechanically. NFR: NFR-COMPAT-API-1.

ReleaseEpics in scope (story priorities)
v0.1 (alpha)E1, E2, E3 (P0 only — lexical), E4, E7, E8, E9 (S1–S6, S9), E10 (S1–S3), E11 (S1–S6), E12, E13 (S1–S4, S6, S7), E14
v0.2 (beta)E5 (P0), E6 (P0–P1), E3 (P1 — semantic/hybrid), E12 (full SSE), E15 (P0–P1), E13 (S5, S8), E16 (S2, S3), E17 (S2, S7 — opt-in skeleton + offline ai_toc)
v1.0 (GA)All P0 across E1–E14 + E17, plus E16 (full gate suite). Documentation site, hosted demo.
v1.1+E10 (S6 diff/rollback), E11 (S7 OIDC), E5 (S5 visual canvas editor), E6 (S4 visual base builder), multi-vault.
  • Functional acceptance criteria live in FR.md. Each story above lists the FR IDs it depends on.
  • Quality gates and measurable targets live in NFR.md. Stories reference NFR IDs where the quality bar is the contract.
  • User journeys (J1–J6) come from PRD.md §6. The mapping below confirms full coverage:
JourneyStories
J1 Agent reads & edits a noteE2-S1, E2-S3, E2-S4, E2-S5
J2 Agent searches the vaultE3-S1, E3-S2, E3-S4, E3-S5
J3 Agent maintains the link graphE2-S7, E4-S1, E4-S3
J4 Human inspects what an agent didE10-S2, E12-S3, E12-S4
J5 Self-hosting setupE13-S1, E13-S2, E13-S3, E13-S4
J6 Existing vault zero-touchE1-S1, E1-S2