8.0 KiB
| created | path | project | status | tags | type | updated | ||||
|---|---|---|---|---|---|---|---|---|---|---|
| 2026-05-11 | Sources/Homelab | herbylab-project-management | active |
|
project-plan | 2026-05-11 |
Goal
Extend herbylab MCP to manage projects and notes as first-class artifacts backed by Postgres, with Trello as the visual projection. Vault remains the immutable content store in Phase 1; Postgres becomes sole source in a later phase.
Solves the pain of status hygiene and project visibility — 44+ vault entries, almost all flagged active, no aggregate view, no real query surface, frontmatter status edits feel wrong.
Locked Decisions
Three-layer model. Mirrors Trellis:
- Vault md files — content, narrative, immutable. Feeds wiki-vault.
- Postgres (
herbylabschema) — state, metadata, queryable index. - Trello — visual projection of project state. Read-mostly.
Single MCP, not split. Project tools live alongside vault tools in herbylab. One connector, one tool surface.
Polymorphic tool shape. create_artifact(kind, ...) and update_artifact(kind, slug, ...) dispatch on kind. Adds future artifact types without growing the tool surface.
Postgres co-located with Trellis. Same instance, separate schema (herbylab.*). Trellis's schema-namespace pattern leaves room for this by design.
Phase 1: dual-write, immutable entries.
create_artifactwrites Postgres row + vault file atomically.- No content updates via MCP — entries immutable once created.
update_artifactonly mutates metadata (status, tags, title, summary, domain).- Vault stays the read surface for Obsidian and wiki-vault.
- New phases/addenda get new files, matching existing pattern (
ob1-deployment+ob1-main-deployment).
Phase N: vault retires, Postgres becomes sole source.
- Viewer TBD — wiki-vault adaptation or new web UI served from Postgres.
- Out of scope for this plan.
Source of truth for status: Postgres. Status changes mid-conversation via update_artifact — never a file edit.
Status propagates via NOTIFY → n8n → Trello. Postgres trigger fires pg_notify('project_changed', ...) on insert/update. n8n LISTENs, projects to Trello.
Frontmatter scope simplifies. Vault frontmatter holds identity (project, type, path, tags, created). Status and updated leave frontmatter — Postgres concerns.
Schema (Phase 1)
CREATE SCHEMA herbylab;
CREATE TYPE herbylab.artifact_kind AS ENUM ('note', 'project');
CREATE TYPE herbylab.project_status AS ENUM ('active', 'paused', 'blocked', 'completed', 'archived');
CREATE TYPE herbylab.project_domain AS ENUM ('homelab', 'dev', 'venture', 'reference');
CREATE TABLE herbylab.artifacts (
slug TEXT NOT NULL,
kind herbylab.artifact_kind NOT NULL,
title TEXT NOT NULL,
domain herbylab.project_domain,
tags TEXT[] DEFAULT '{}',
vault_path TEXT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (kind, slug)
);
CREATE TABLE herbylab.projects (
slug TEXT PRIMARY KEY,
status herbylab.project_status NOT NULL DEFAULT 'active',
summary TEXT,
trello_card_id TEXT,
FOREIGN KEY (kind, slug) REFERENCES herbylab.artifacts (kind, slug)
DEFERRABLE INITIALLY DEFERRED
);
CREATE INDEX idx_artifacts_kind ON herbylab.artifacts(kind);
CREATE INDEX idx_artifacts_tags ON herbylab.artifacts USING GIN(tags);
CREATE INDEX idx_projects_status ON herbylab.projects(status);
body stores the markdown blob. Vault file holds the same content. Dual-write keeps them in lockstep; immutability eliminates drift risk.
NOTIFY trigger
CREATE OR REPLACE FUNCTION herbylab.notify_project_change() RETURNS TRIGGER AS $$
DECLARE payload JSON;
BEGIN
payload := json_build_object(
'op', TG_OP,
'slug', COALESCE(NEW.slug, OLD.slug),
'status', COALESCE(NEW.status, OLD.status)
);
PERFORM pg_notify('project_changed', payload::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER projects_notify
AFTER INSERT OR UPDATE OR DELETE ON herbylab.projects
FOR EACH ROW EXECUTE FUNCTION herbylab.notify_project_change();
Tool surface
Vault tools (existing, unchanged):
save_vault_noteget_vault_projectcheck_vault_filelist_vault_projectsget_vault_schema
Artifact tools (new):
create_artifact(kind, slug, title, vault_path, content, domain?, tags?, summary?)— writes Postgres row + vault file. Forkind="project", also insertsprojectsrow with defaultstatus='active'.update_artifact(kind, slug, **fields)— mutates metadata only (status, title, tags, summary, domain). Never touches body or vault file.get_artifact(kind, slug)— returns row + linked file path.list_artifacts(kind?, status?, domain?, tag?)— filtered query.
list_vault_projects deprecates in favor of list_artifacts once cutover completes.
Open Items
- Postgres deployment shape — depends on Trellis decision (LXC vs container)
- Migration tooling — match whatever Trellis lands on
- Trello board layout — single board with status lists vs board per domain
- Duplicate cleanup before backfill:
ob1-deployment+ob1-main-deployment,editor-stack-upgrade(Homelab + Venture),content-hub-phase5-architecture+-planning - Naming pin-down: memory has both
herbylabandherby-dev— settle on one
Phases
Phase 1 — Infrastructure
- Postgres instance available with
herbylabschema on the Trellis-shared instance - Migration tooling chosen (match Trellis decision)
- Apply schema, types, indexes, NOTIFY trigger
Phase 2 — MCP tools
create_artifact(dual-write, transactional)update_artifact(metadata only)get_artifact,list_artifacts(read paths)- Tag normalization mirroring Trellis pattern
Phase 3 — Trello projection
- Trello board structure: lists for
active,paused,blocked,completed. Labels for domain. - n8n workflow: Postgres LISTEN node on
project_changed - Branch on op: INSERT → create card, UPDATE → move/edit card, DELETE → archive
- Store
trello_card_idback toprojectsrow on creation
Phase 4 — Backfill and cutover
- Script walks existing vault project-plan files, populates
artifacts+projectsrows - Status defaults to current frontmatter value (42 of 44 are
active) - Manual triage pass in Trello — drag to honest columns
- Strip
status/updatedfrom vault frontmatter going forward - Backfill of existing frontmatter optional
Phase N — Vault retirement
- Deferred. Revisit once Phase 1–4 are operational and read patterns are clearer.
Notes
Why dual-write in Phase 1. Keeps two readers (Obsidian, wiki-vault) functional during transition. Immutability of entries kills the main drift risk — entries land once, neither side updates content. Update tool only touches metadata, which lives only in Postgres.
Why immutable entries. Matches existing behavior — new phases get new files (-phase2, -addendum) rather than edits. Codifying this as a constraint removes a class of bugs and makes Phase N (Postgres-only) trivially achievable later.
Why Postgres co-located with Trellis. Trellis's plan explicitly chose schema-namespacing to leave room for consolidation. Same instance, separate schemas, no new infrastructure. Backup/recovery story is shared.
Why not Trellis itself for project tracking. Trellis is for conversation threads — coarse session-level checkpoints, append-only events, paused/resumed work. Projects are different: durable, named, status-bearing, visualized externally. Different lifecycle, different reader, different surface.
Why the polymorphic tool shape. Avoids tool sprawl. Adding a future artifact type (task, decision log, etc.) is a kind value, not a new MCP tool. Tradeoff is a fatter schema in the tool description.