* feat(lifecycle): revision findings ledger — structured QA/PR/PM/CEO failure feedback, persisted and delivered down the chain Every bounce used to survive only as flattened prose: rounds overwrote each other in notes_structured, request_changes persisted nothing, two raw dev_notes appends were silently destroyed by the next handoff note, and the dev prompt pointed at fields (qa_notes via evidence(), pm_notes) the API never delivered. Agents re-interpreted and re-discovered every failure before they could start fixing it. - task_review_findings (migration 071, append-only): file/line/severity/ criterion(AC-id-validated)/expected/actual/fix/evidence per finding, with origin (qa|pr_gate|pm|ceo), round, and an open->addressed->verified lifecycle (waived reserved); new tasks.pm_notes + PmReviewContent give request_changes a structured home - producers: fail_review/pr_fail/request_changes take findings=[...] (prose issues shimmed+merged for one release, deprecation-logged); ceo_reject validates its reason (no 500), lands an origin=ceo finding, and bumps round+audit on branchless coordination roots; guardrails at the verb chokepoint (nudge >5, hard reject >10, field caps, traversal-safe file); the dev_notes data-loss appends are removed; new task.request_changes + task.ceo_reject audit events close rework attribution - delivery: qa_notes/pr_reviewer_notes/pm_notes carry the deterministic [F-id8] rendering; claim briefings, evidence(), the REVISION_REQUIRED spawn prompt, PM triage bounced-blocks, and A2A bodies deliver open findings; round-N+1 QA and gate reviewers get the full prior ledger; panel Findings tab + bounced-xN chip; metrics pm_rejects/ceo_rejects + findings counts; vault task notes render a Findings section (fail-open) - resolution closes for every origin: i_am_done and submit_up/submit_root take resolved_findings gated by FINDINGS_ADDRESSED (owner-gated so a stale non-owner PM can never mutate the ledger); pass_review/pr_pass/ complete verify-stamp same-transaction; ceo_approve stamps best-effort - 24 real-DB integration tests drive the full loop through the real choreographer; full suite 12856 green * docs: revision findings ledger sweep — CLAUDE.md, map, RAG corpus - CLAUDE.md: new ledger section + corrected request_changes row - docs/map/review-findings.md (new subsystem map) + surgical updates to task-service/pr-gate-review/metrics-observability/vault/panel maps - docs/rag: producers' findings contract across qa/pr-reviewer/developer/ cell-pm/main-pm/ceo role docs (the PM docs were missing request_changes entirely), verb references, and a new architecture/review-findings.md disambiguating ledger findings from convention findings * test(e2e): resubmit resolves the pr_fail finding per the ledger contract The scripted pr_fail revision loop resubmitted submit_up without resolved_findings — correctly rejected now that FINDINGS_ADDRESSED gates the PM resubmit verbs (green locally, red only in CI since the e2e suite skips without ROBOCO_E2E_SMOKE=1). The scripted PM now reads the open ledger row pr_fail persisted (new open_finding_ids arc helper) and resolves it on resubmit, asserting the open set drains — exercising the coordinator half of the new contract end to end. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
20 KiB
Purpose
The Obsidian vault (V1+V2): 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. V2 adds three things on top of the V1 projection: materialize-on-create (a task's note exists from the moment it's created, not just at curation/rebuild), a drift janitor (hourly-ticked, daily/weekly-gated: re-projects changed tasks, verifies a random sample, archives old terminal tasks, writes the weekly org-report), and KB ingest (the CEO's own RoboCo/Notes/ notes become one more RAG corpus the fleet can retrieve). Default-off (ROBOCO_OBSIDIAN_VAULT_ENABLED; both compose files arm it true). Still structurally different from the other default-off engines: the projection never originates delivery work itself — 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 / write_org_report. 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. V2: write_task is archive-aware (TaskNoteData.archive_year routes it to RoboCo/Archive/<year>/Tasks/<project>/ instead of Tasks/<project>/, removing the stale copy on a move); find_task_note/task_note_status locate/inspect a note wherever it lives (recursive id8 lookup across both trees) for the janitor's drift check; write_org_report renders RoboCo/Reports/<ISO-week>.md. (Uncommitted, feature/findings-ledger) FindingRow + _FINDINGS_CAP=20 + _findings_section render a ## Findings section (one [F-id8] (severity, round N, status) file:line — expected → actual → fix line per open/resolved finding, an overflow line past the cap) into every task note — see docs/map/review-findings.md. |
534 |
roboco/services/vault_assembly.py |
assemble_task_note_data — resolves a task's project slug, parent, subtasks, dependencies, and (V2) archive eligibility (_archive_year, gated on vault_archive_days) via the live TaskService/ProjectService into a TaskNoteData. reproject_task (V2) bundles assemble + narrative-preservation + write_task into the one code path shared by rebuild, the janitor's changed/sample/archival passes, and the create-on-task seam — none of them can drift on how a note gets refreshed. (Uncommitted, feature/findings-ledger) _resolve_findings fetches the task's ledger via ReviewFindingsRepository.list_for_task, fails open (empty tuple) on a missing session or any exception — a findings-fetch failure drops the section, never blocks the note write. |
~125 |
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). V2: the frontmatter split + content-hash helpers moved to the shared foundation/policy/vault_notes.py (this module now just imports them). |
~350 |
roboco/services/vault_janitor.py (new, V2) |
VaultJanitor.run_cycle — one state-gated sweep: re-project tasks changed since the last sweep (TaskService.list_updated_since, capped/paged, per-item isolated), verify a random stale sample (sample_stale_tasks), archive old terminal tasks (list_archive_candidates), and (weekly) render the org-report. Dueness is tracked in a JSON state file (RoboCo/_meta/.janitor_state.json: last_sweep, last_report_week, archive_watermark), not the loop's own cadence — restart-proof. |
~343 |
roboco/services/vault_kb_engine.py (new, V2) |
VaultKBEngine.run_cycle — scans the allowlisted vault_kb_dirs (default RoboCo/Notes), dedups by content hash, screens every note body through the injection guard as a hard GATE (flagged → quarantined, never indexed), and ingests/deindexes into IndexType.VAULT_NOTES via OptimalService.index_vault_note/unindex_vault_note. Defense-in-depth containment: symlinks and any resolved-path escape from the vault root are skipped, independent of the config-load validator. |
~315 |
roboco/foundation/policy/vault_notes.py (new, V2) |
Shared pure helpers: content_hash (sha256 with every > [!kind] RoboCo: ... feedback callout stripped first, so appending one doesn't change what the next scan considers "changed") and split_frontmatter (YAML frontmatter + body). Used by both the intake watcher's "drafted" callout and the KB engine's "quarantined" callout — one shared convention instead of two copies drifting. |
~46 |
roboco/services/optimal_brain/indexes/vault_notes.py (new, V2) |
VaultNotesIndexPlugin — IndexType.VAULT_NOTES plugin, mirrors PlaybooksIndexPlugin's shape (index_note/delete_note/search_notes, source URI vault://<relpath>). Scope enforced by the KB engine's dir allowlist, not this plugin. |
~70 |
roboco/vault.py |
python -m roboco.vault {rebuild,relocate} CLI. rebuild re-projects every agent/task/journal-entry/A2A-thread from the DB (now archive-aware via vault_assembly.reproject_task — an old terminal task projects straight into Archive/<year>/) 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 — V2 adds Archive/Reports graph color groups) + meta/ (dashboard + kanban-board + README, V2 adds Task Board.base + Reports.base for Obsidian's core Bases plugin, and Sync to your Mac.md, the Syncthing/SMB/Obsidian-Sync runbook). Dataview dashboard queries now exclude Archive/. |
— |
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. V2 reuses it a third time: the KB engine's ingest-time hard gate (quarantine on a hit, vs. the intake watcher's screen-and-still-process posture). |
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. |
— |
roboco/services/task.py _materialize_vault_note / list_updated_since / list_archive_candidates / sample_stale_tasks |
V2: the create-time seam + the janitor's three query methods. See docs/map/task-service.md. |
— |
Data Flow
PROJECTION (always-on when the flag is armed). TaskService.create (V2) calls _materialize_vault_note — best-effort, same swallow-and-log posture as every other seam — so a task's note exists from the moment it's created, not just at curation/rebuild. Three more best-effort event seams fire from existing services: TaskService._emit_status_transition_audit → _touch_vault_frontmatter patches an EXISTING note's status/team/pr fields in place (now effectively always finds one for any task created post-V2, since materialize-on-create ran; a pre-V2 task without a note is still a no-op here — the janitor's changed/sample passes are what backfill it); 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 import get_vault_writer() lazily and catch every exception.
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 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; skip if already seen (path + content-hash in vault_seen_notes); screen the body via screen_external_text; extract title/description/action-items via a local-model chat call against the SCREENED text, falling back to deterministic extraction; open ONE PENDING task (source=vault_note, Product-Owner-assigned, team=board) capped by vault_intake_max_open_drafts/vault_intake_max_per_cycle; append a feedback callout (best-effort). Never starts delivery directly — the board-review path is the only door.
JANITOR (V2, hourly-ticked, day/week-gated). AgentOrchestrator._vault_janitor_loop (gated on obsidian_vault_enabled alone) ticks every JANITOR_LOOP_INTERVAL_SECONDS (3600, no config knob) and calls VaultJanitor.run_cycle. Actual work only happens when the restart-proof state file (RoboCo/_meta/.janitor_state.json) says it's due:
- Sweep (due when
last_sweepis >= 24h stale):_reproject_changedre-projects every task touched since the last sweep (TaskService.list_updated_since, ascending, paged 100 at a time, capped at_MAX_REPROJECT_PER_CYCLE=200per tick, one bad item logged-and-skipped rather than wedging the pass) via the sharedreproject_task;_verify_samplepulls a random 20-task sample of tasks last touched before the sweep window (sample_stale_tasks) and repairs any whose note is missing or whose frontmatter status disagrees with the DB (viatouch_task_frontmatter, not a full re-projection);_archive_passmoves terminal tasks pastvault_archive_daysintoArchive/<year>/(see below). A capped tick advanceslast_sweep/archive_watermarkonly to the last-processed item's stamp (not "now"), so the very next hourly tick — already due again — picks up the tail with no gap. Logs onevault_drift_repairedline:count(repaired) /archived/failed. - Weekly report (due when
last_report_week!= the current ISO week, andvault_report_enabledis on):_run_weekly_reportpullsMetricsService.get_velocity/get_cycle_time_by_stage/get_bottleneck_distribution/get_rework_metrics(days=7) +UsageService.get_summary("7d"), rendersVaultWriter.write_org_report, and best-effort notifies the CEO (NotificationService.send_weekly_report_notification) — a notification failure never invalidates the already-written note.
ARCHIVAL (V2, folded into the janitor sweep). Policy: a terminal (completed/cancelled) task whose terminal timestamp (completed_at else updated_at else created_at) is older than vault_archive_days (default 30; 0 disables archival outright) moves from RoboCo/Tasks/<project>/ to RoboCo/Archive/<year>/Tasks/<project>/. TaskService.list_archive_candidates(after, before, ...) returns terminal tasks whose terminal timestamp falls in [watermark, cutoff), paged/capped identically to the changed-task pass (_MAX_ARCHIVE_PER_CYCLE=200). The move itself is free: VaultWriter.write_task is archive-aware (TaskNoteData.archive_year set by vault_assembly._archive_year) — it looks up an existing note across BOTH Tasks/ and Archive/ by id8, writes the new copy at the archive-aware target directory, and deletes the stale copy if it moved. Alias-based wikilinks ([[id8|title]]) mean nothing pointing at an archived task ever breaks. rebuild is archive-aware for free (routes through the same reproject_task), and the shipped Dataview dashboard + graph color groups exclude Archive/.
KB INGEST (V2, independently double-gated). AgentOrchestrator._vault_kb_loop (BOTH obsidian_vault_enabled AND vault_kb_enabled required) ticks VaultKBEngine.run_cycle every vault_kb_interval_seconds (default 900). Per allowlisted dir in vault_kb_dirs (default RoboCo/Notes; config-load validation in Settings._validate_vault_kb_dirs rejects an absolute/..-carrying entry or one overlapping vault_intake_dir or a reserved projection dir): recursively scan *.md, skip a note that's a symlink, escapes the resolved vault root, or exceeds 64KB; content-hash-dedup against the currently-tracked IndexType.VAULT_NOTES docs (an unchanged note is skipped); screen the frontmatter-stripped body through screen_external_text as a hard GATE — a flagged note is quarantined (skipped, warn-logged, a one-line feedback callout appended, and any PRIOR indexed chunks removed if it was previously clean) rather than indexed; a clean note ingests via OptimalService.index_vault_note (bounded to _MAX_INGEST_PER_CYCLE=50 per tick — the tail waits for the next cycle). A deletion pass deindexes any previously-tracked path no longer seen on disk. Consumers: roboco_kb_search picks up VAULT_NOTES for free once the enum exists; MentorService's default/general domain search list includes it (labeled "Vault Notes"); EvidenceRepo.similar_memory includes it in claim-time briefings (kind vault_note), same relevance floor as learnings/playbooks; the panel's KB browser has a full type entry (nav/filter/badge/stats).
REBUILD/RELOCATE (operator/CLI, not agent-facing). python -m roboco.vault rebuild walks every agent, then every task (via the shared reproject_task — archive-aware, narrative-preserving), then every non-private journal entry, then every A2A thread, 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.
Config Flags
ROBOCO_OBSIDIAN_VAULT_ENABLED— master switch; off =VaultWriteris never invoked from any seam,curate_vaultreturnsinvalid_state, the janitor/KB loops return immediately, andpython -m roboco.vaultrefuses. Config defaultfalse; both compose files set ittrue.ROBOCO_VAULT_PATH(default/data/vault) — root directory the vault materializes into; bind-mounted in both compose files.ROBOCO_VAULT_INTAKE_ENABLED— independent switch for_vault_intake_loop; inert unless the master switch is ALSO on. Config defaultfalse; both compose files set ittrue.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.ROBOCO_VAULT_ARCHIVE_DAYS(default30,0disables) — age past which a terminal task's note archives during the janitor sweep. Checked only under the master switch — no separate enable flag.ROBOCO_VAULT_REPORT_ENABLED(defaulttrue) — the janitor's weekly org-report + CEO notification. Config defaulttruein both compose files (deterministic, no LLM, cheap to leave on).ROBOCO_VAULT_KB_ENABLED(defaultfalse) — master switch for KB ingest; off =_vault_kb_loopreturns immediately andIndexType.VAULT_NOTESstays empty. NAS compose (docker-compose.yml) sets ittrue; the public registry compose (docker-compose.registry.yml) leaves itfalse(optional engines ship off).ROBOCO_VAULT_KB_DIRS(defaultRoboCo/Notes, CSV) — vault-relative folders the KB engine scans. Rejected at config load if absolute,..-carrying, or overlappingvault_intake_dir/Tasks/Journals/A2A/Agents/Archive/Reports/_meta/.obsidian.ROBOCO_VAULT_KB_INTERVAL_SECONDS(default900, min60) — KB-engine scan cadence.
Health
The projection side is zero-risk by construction: every seam is best-effort and DB-free from the writer's perspective, so a filesystem or permission failure degrades to a stale/missing note, never a blocked verb. Materialize-on-create closes the V1 gap where the Dataview board only ever showed curated/rebuilt tasks — a fresh task is visible immediately. The janitor is the freshness backstop for everything best-effort seams can miss: it's restart-proof (dueness lives in a state file, not loop cadence — an orchestrator that restarts more often than daily still sweeps exactly once per elapsed day), self-healing against a corrupt/hand-edited state file (any unparseable value degrades to "no state," never a wedged loop), and every per-item drain (changed-task, sample-verify, archive) isolates failures — one bad row is logged and skipped, never aborts the pass, and re-qualifies on its next change or the next sample draw. Per-cycle caps (200 reprojects, 200 archives) mean a first-enable or long-downtime backlog drains in bounded hourly slices via the resume-marker convention (a capped tick advances the marker only to the last item it actually processed) rather than one unbounded burst.
The KB-ingest side is the one path with real security stakes — once a vault note is agent-retrievable, unscreened note text is injection into the fleet's retrieval context, not just a drafting risk. It layers defense-in-depth: the config-load validator rejects a dangerous vault_kb_dirs entry outright (can't even start with an escaping/overlapping dir); the engine independently re-checks every allowlisted dir resolves under the vault root before scanning it; every individual note is re-checked for symlink-ness and resolved-path escape before it's read (belt-and-suspenders against a dir-level check being bypassed by a per-file symlink); and the injection guard runs as a hard GATE (not the intake watcher's screen-and-still-process posture) — a flagged note is never embedded, only quarantined with a visible callout so the CEO knows why. Content-hash dedup (shared with the intake watcher's ledger convention) makes both re-scans and the quarantine callout's own append idempotent — appending the callout never itself re-triggers reprocessing.
Rebuild/relocate remain idempotent and additive-safe (ensure_vault_assets never overwrites an existing file), so re-running against a CEO-customized vault cannot clobber .obsidian//_meta/ edits.
Related
docs/rag/architecture/obsidian-vault.md— the agent-facing doc (what the Auditor and vault-intake-originated tasks actually see, plus what changed for KB retrieval)docs/rag/roles/auditor.md— thecurate_vaultverbdocs/map/orchestrator.md—_dispatch_vault_curation_work/_maybe_spawn_vault_curation/_vault_intake_loop/_vault_janitor_loop/_vault_kb_loopdocs/map/task-service.md—_materialize_vault_note/list_updated_since/list_archive_candidates/sample_stale_tasksdocs/map/product-strategy-research-pitch.md—XEngine, the sibling engine sharinginjection_guard.screen_external_textdocs/internal/specs/2026-07-09-obsidian-vault.md— the original V1 design spec (vault layout, link-stability rationale)docs/internal/specs/2026-07-11-obsidian-vault-v2.md— the V2 spec (materialize-on-create, janitor, archival, KB ingest, weekly report, Bases, sync doc)