Files
roboco/docs/map/vault.md
T
5a0fce7da4 docs: v0.23.0 agent-facing sweep — map, RAG corpus, CLAUDE.md (#468)
New map + RAG entries for the vault subsystem; sequence gate, lineage
merge, gate diff-base, CI guard, playwright MCP, dispatcher prefilter,
backup sidecar, and the 300/100 budget reflected across docs/map,
docs/rag, and CLAUDE.md; stale claims fixed (agent-ux 'no extra tools',
old budget defaults). No redirects needed — nothing publicly published
moved.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 10:19:32 +02:00

9.6 KiB

Purpose

The Obsidian vault V1: a rebuildable, human-readable DB projection of the org's memory (tasks, journal entries, A2A thread digests) as wikilinked markdown, plus a default-off inbox watcher that turns #roboco-tagged vault notes into board-review intake drafts. Default-off (ROBOCO_OBSIDIAN_VAULT_ENABLED; both compose files arm it true). Mirrors the other default-off engines' shape in one respect (event-driven, best-effort, never a gate) but is structurally different: it never originates delivery work itself — the projection is read-only from the DB's perspective, and the ONE writer-side effect that reaches delivery (the intake watcher) rides the existing board-review path, not a held-artifact queue.

Files

Path Role approx LOC
roboco/services/vault_writer.py VaultWriter — pure, DB-free markdown materializer. write_task / write_journal_entry / append_a2a_message / write_agent / touch_task_frontmatter. Every note carries aliases: [<id8>] so a rename never breaks a [[id8|title]] wikilink; existing_narrative reads back an Auditor-authored ## Narrative section so a rebuild never clobbers it. 371
roboco/services/vault_assembly.py assemble_task_note_data — resolves a task's project slug, parent, subtasks, and dependencies via the live TaskService/ProjectService into a TaskNoteData for VaultWriter.write_task. Shared by curate_vault and the CLI rebuild; kept separate from vault_writer so that module stays DB-free and trivially unit-testable. 87
roboco/services/vault_intake_engine.py VaultIntakeEngine.run_cycle — scans the vault's inbox dir for #roboco-tagged notes, dedupes via vault_seen_notes (path + content-hash), screens the body through injection_guard.screen_external_text, extracts a title/description/action-items via a local-model chat call (deterministic fallback: first heading / raw body / checkbox lines), and opens ONE PENDING board-review draft (source=vault_note, Product-Owner-assigned, team=board) per note. Appends a feedback callout back into the note (best-effort). 373
roboco/vault.py python -m roboco.vault {rebuild,relocate} CLI. rebuild re-projects every agent/task/journal-entry/A2A-thread from the DB and materializes .obsidian/ + RoboCo/_meta/ from roboco/vault_assets/ (never overwrites an existing file). relocate <path> moves the tree; grafts RoboCo/ into an existing destination vault without touching its own config. 237
roboco/vault_assets/ Packaged templates copied by ensure_vault_assets: .obsidian/ (Dataview, Kanban, graph-group config) + meta/ (dashboard + kanban-board + README shipped into RoboCo/_meta/).
roboco/foundation/policy/injection_guard.py screen_external_text / detect_injection — the shared prompt-injection screen-and-neutralize (data path) and hard-deny (interactive-input path) pattern set, used by both the vault intake watcher and XEngine's mentions poll. 125
roboco/services/gateway/content_actions.py curate_vault Server-side do-action: Auditor-only, re-materializes a task's note with the Auditor's narrative filling ## Narrative. Inert (invalid_state) when the flag is off.
roboco/mcp/do_server.py curate_vault Do-tool the Auditor calls exactly once per completed root, POSTing to /api/v1/do/curate_vault.
roboco/db/tables.py VaultSeenNoteTable Dedup ledger for the intake watcher: (note_path, content_hash) — an unchanged note is never reprocessed; an edited one is eligible again.

Data Flow

PROJECTION (always-on when the flag is armed). Three best-effort event seams fire from existing services, never gating the real action: TaskService._emit_status_transition_audit_touch_vault_frontmatter patches an EXISTING note's status/team/pr fields in place (a not-yet-materialized note is a silent no-op — full materialization happens at Auditor curation or CLI rebuild); JournalService's entry-write path → _materialize_vault_note writes one immutable file per non-private entry; A2AService.send_materialize_vault_note appends to a per-thread digest file, deduped per message id via an in-body marker comment. All three import get_vault_writer() lazily and catch every exception — a vault failure is logged and swallowed, never surfaced to the caller.

CURATION (root-completion hook, orchestrator-driven). AgentOrchestrator._dispatch_vault_curation_work (one of the 18 tick dispatchers, gated on obsidian_vault_enabled) reads TaskService.list_completed_roots_pending_vault_curation and calls _maybe_spawn_vault_curation per candidate: an in-memory one-shot guard (_board_dispatched) plus a durable vault_curation_dispatched marker (survives a restart) precede a bindingless Auditor spawn — the root task id is named in the prompt rather than bound as the spawn's active task (the root is already completed, so binding would trip the readiness gate's role-for-status check). The Auditor writes one narrative paragraph and calls curate_vault(task_id, narrative) exactly once; the verb re-resolves the task's parent/subtasks/dependencies fresh via assemble_task_note_data and fully re-materializes the note, filling the ## Narrative section a deterministic write otherwise leaves as _Pending Auditor curation._.

INTAKE (independently-gated inbox watcher). AgentOrchestrator._vault_intake_loop (both obsidian_vault_enabled AND vault_intake_enabled required) ticks VaultIntakeEngine.run_cycle every vault_intake_interval_seconds. Per note under the inbox dir: skip if no #roboco tag (frontmatter list or inline #roboco token); skip if already seen (path + content-hash in vault_seen_notes — an edit changes the hash and re-qualifies); screen the body via screen_external_text (wraps it in an untrusted-content envelope, flags any injection-pattern line inline, never removes content); extract title/description/action-items via a local-model chat call against the SCREENED text, falling back to a deterministic first-heading/raw-body/checkbox-line extraction on any local-model failure; open ONE PENDING task (source=vault_note, Product-Owner-assigned, team=board, confirmed_by_human=True — the board routing IS the start gate) capped by vault_intake_max_open_drafts (rolling) and vault_intake_max_per_cycle (per-tick); append a feedback callout into the source note (best-effort). The draft NEVER starts delivery directly — it takes the identical intake board-review path a chat-confirmed draft would: Product Owner + Head of Marketing review, and only the CEO's approve_and_start hands it to the Main PM.

REBUILD/RELOCATE (operator/CLI, not agent-facing). python -m roboco.vault rebuild walks every agent, then every task (preserving each task's existing ## Narrative via writer.existing_narrative before re-materializing), then every non-private journal entry, then every A2A thread — fully re-projecting from the DB — and materializes the shipped .obsidian//_meta/ assets if absent. relocate <path> moves RoboCo/ into a destination, refusing if the destination already has a RoboCo/ subtree (avoids clobbering another vault's own projection).

Config Flags

  • ROBOCO_OBSIDIAN_VAULT_ENABLED — master switch; off = VaultWriter is never invoked from any seam, curate_vault returns invalid_state, and python -m roboco.vault refuses. Config default false; both docker-compose.yml and docker-compose.registry.yml set it true.
  • ROBOCO_VAULT_PATH (default /data/vault) — root directory the vault materializes into; bind-mounted in both compose files.
  • ROBOCO_VAULT_INTAKE_ENABLED — independent second switch for _vault_intake_loop; inert unless the master switch is ALSO on. Config default false; both compose files set it true.
  • ROBOCO_VAULT_INTAKE_INTERVAL_SECONDS / ROBOCO_VAULT_INTAKE_DIR / ROBOCO_VAULT_INTAKE_MAX_PER_CYCLE / ROBOCO_VAULT_INTAKE_MAX_OPEN_DRAFTS — cadence, inbox subfolder, per-cycle origination cap, rolling open-draft cap.

Health

The projection side is zero-risk by construction: every seam is best-effort and DB-free from the writer's perspective (VaultWriter takes plain dataclasses, never touches the DB itself), so a filesystem or permission failure degrades to a stale/missing note, never a blocked verb. The intake side is the one path that can reach delivery, and it is triple-gated: the flag pair, the injection screen ahead of both the local-model prompt and the deterministic fallback (so neither ever sees raw unscreened text), and the board-review path itself (a vault_note draft is PENDING + board-assigned, never auto-started — the CEO's approve_and_start is the only door to the Main PM). The dedup ledger (vault_seen_notes) is content-hash keyed, not just path-keyed, so an edited note is correctly re-eligible without needing a delete+recreate. Rebuild is idempotent and additive-safe (ensure_vault_assets never overwrites an existing file), so re-running it against a vault the CEO has already customized cannot clobber their edits to .obsidian//_meta/.

  • docs/rag/architecture/obsidian-vault.md — the agent-facing doc (what the Auditor and vault-intake-originated tasks actually see)
  • docs/rag/roles/auditor.md — the curate_vault verb
  • docs/map/orchestrator.md_dispatch_vault_curation_work / _maybe_spawn_vault_curation / _vault_intake_loop
  • docs/map/product-strategy-research-pitch.mdXEngine, the sibling engine sharing injection_guard.screen_external_text
  • docs/internal/specs/2026-07-09-obsidian-vault.md — the original design spec (vault layout, link-stability rationale)