# Plan: Plugin Knowledge-Quality Enforcement Status: **active** (opened 2026-06-01). Raises the demarkus-memory Claude Code plugin from "advisory knowledge system" to "enforced knowledge system," and aligns the per-project template to the proven `demarkus-soul` structure. Owner: Fritz. Driver: agent. **Release strategy (Fritz 2026-06-01):** stack ALL plugin work onto one branch (`feat/plugin-publish-tag-gate`, PR #171) and ship a single `0.4.0` release rather than churning many plugin versions. Review the code at each step, but no per-item version bump. ## Problem The plugin splits into two layers at very different maturity: - **Infra layer** (binary install, server spawn, token mint, version pins, health checks): solid, deterministic, harness-executed. - **Knowledge-quality layer** (`context/session-guidance.md`, the `soul-memory` skill, routing + tagging rules): **100% advisory prose executed by the model on best effort.** Nothing verifies any of it happened. Every load-bearing quality assumption — tag every publish, recall before answering, route content to the right file, journal significant sessions — is hope, not enforcement. The server "infers nothing." So quality degrades silently and invisibly: a tagless publish is accepted happily and becomes permanently invisible to `mark_lookup`. That is the single most damaging hole, because the entire recall half of the system depends on `tags` the agent must remember to set by hand while focused on content. ## Strategy Move enforcement off advisory prose and onto **hooks**, which the harness executes deterministically. Pure composition of existing surface — no core changes (consistent with the core-vs-overlay layering rule: the server stays permissive; the agent-driven client gets the guardrail). Severity is configurable but the **check always runs** — this is a strictness *level*, not an opt-in capability (no "validation off" deployment exists): - `warn` (default) — PostToolUse injects a system-reminder after a tagless publish, nudging a re-publish with tags. Non-blocking. - `block` — PreToolUse denies a tagless publish until tags are added. - `ask` — PreToolUse escalates to the user. ## Hook mechanics (verified against code.claude.com/docs/en/hooks) - PreToolUse: `hookSpecificOutput.permissionDecision` in `allow|deny|ask|defer` + `permissionDecisionReason`. Carries `block`/`ask`. - PostToolUse: `hookSpecificOutput.additionalContext` injects a system-reminder the model sees next turn. Carries `warn`. (Plain stdout is debug-only, not model-visible.) - Stop: only top-level `{"decision":"block","reason":...}` surfaces anything (no confirmed `additionalContext`). So a Stop nudge necessarily blocks one extra turn. Input carries `session_id`, `transcript_path`, `stop_hook_active`; loop-guard on `stop_hook_active` plus a per-session sentinel. - Matchers are JS regex against the full `mcp__server__tool` name. Scope to `mcp__.*demarkus-memory__mark_publish` so it hits the plugin's own server, not the user's other demarkus servers (soul/hub/knowledge). Stop takes no matcher. - One dispatcher script handles both publish events; branches on `hook_event_name`, reads `STRICTNESS`. **Zero runtime dependencies** — payloads parsed by pure-awk character-level JSON scanners (no jq, no python; awk is already the engine behind `json_escape`). A naive grep would false-match the arbitrary `body` value, so the scanner tracks string boundaries + the object-key path and reads only the exact fields it needs. **Fails open** — any parse hiccup defers, never blocks. ## Work items ### 1. Publish tag/importance gate — DONE 2026-06-01 (PR #171) `hooks/publish-gate.sh` (one dispatcher on both PreToolUse + PostToolUse) + matcher wiring in `plugin.json`. Tagless publish (`tool_input.metadata.tags` absent/empty/whitespace, or a non-string `tags` value) or `importance` outside [0,1] → warn/block/ask per strictness. Strictness read by `configured_strictness` in `lib.sh`: `DEMARKUS_MEMORY_STRICTNESS` env override → `~/.demarkus/plugin-memory.strictness` file → `warn` default (kept in its own file, orthogonal to `plugin-memory.conf`, so `setup.sh` rewrites never clobber it). Parser is the pure-awk `publish_metadata_check` in `lib.sh`. Pins bumped SERVER 0.17.13→0.17.14 (client/tools already latest). Plugin version 0.3.0→0.4.0. CodeRabbit review surfaced four real correctness holes in the awk parser, all fixed + regression-tested: - escaped-whitespace bypass — `{"tags":"\n"}` was stored raw (backslash+n) and passed; added a pure-awk `json_unescape` (decodes `\n \t \r \b \f \/ \" \\` and `\uXXXX`; whitespace codepoints → space, others → non-space marker) applied before trim/range checks. - non-string `tags` bypass — `{"tags":false|null|0}` and array/object values passed; `recordValue` now tracks whether the value came from a JSON string (`tagsStr`) and `tags_ok` requires it. - exponent `importance` — `1e-1` was wrongly rejected; regex now allows `([eE][+-]?[0-9]+)?`. - escaped-whitespace + array/object test coverage added. Publish suite now 22 cases, green. Follow-up noted: the plugin's shell tests (`tests/*.sh`) are not run by any CI workflow today — `knowledge-join_test.sh` is manual too. Wiring them into CI is a separate, small task (touches `.github/workflows/`, mind the YAML `${{ }}`-in-comments rule). ### 2. Relocate decaying guidance to the moment of action — DONE 2026-06-01 (PR #171) `session-guidance.md` tagging block tightened: the gate now carries the tagging rule at write time, so the startup prose states it tersely, points at the gate, and makes the enforced-vs-advisory split explicit. (Reconciled in item 3 to note journaling now has a soft session-end backstop; in item 6 to point routing at the canonical template.) ### 3. Session-end journal nudge (Stop hook, guarded) — DONE 2026-06-01 (PR #171) `hooks/session-journal-nudge.sh` (Stop hook, no matcher). Nudges once, at session end, when the session **changed files** (`Edit`/`Write`/`NotebookEdit` tool_use in the transcript — a low-false-positive "real work" signal) but **recorded nothing** to any demarkus memory store (no `mark_publish`/`mark_append` via MCP attribution or tool name). Since a Stop hook can only surface a message by blocking, it emits `{"decision":"block","reason":...}` once with an explicit "if it's routine, just stop" out. Two loop guards: `stop_hook_active==true` (also tolerates a quoted `"true"`) early-exits, and a per-session sentinel fires at most once per session. New parser `stop_hook_fields` in `lib.sh` (pure awk). Tests: `tests/session-journal-nudge_test.sh`, 11 cases. Open question for review: the blocking Stop nudge forces one extra turn per qualifying session. Acceptable + bounded (once per session, easy out), but if it proves annoying, add a disable knob (e.g. `DEMARKUS_MEMORY_JOURNAL_NUDGE=off` / strictness file) — deferred until there's signal it's needed. ### 4. Recall-before-answering (deferred until 1-3 land) Hardest, most false-positive-prone. Good capture is the precondition for good recall — fix tags first. Cheap version: UserPromptSubmit pattern-match ("did we / what did / last time / decide") → one-line "check the soul" nudge. ### 5. Catalog hygiene tooling (`/soul-doctor`) Surfaces existing rot: orphan docs (no inbound links, not in any index), broken `[[links]]`, duplicate content, stale index entries, ADR numbering collisions. Built on `mark_backlinks` / `mark_graph`. Tooling, not enforcement. ### 6. Align local per-project template to the proven `demarkus-soul` structure — DONE 2026-06-01 (PR #171) Single source of truth: `seed/project-template.md`, seeded to the soul root as `/project-template.md` (via the generalized `seed_doc` in `lib.sh`, called from `session-start.sh`). It never clobbers, so a user-customized `/project-template.md` persists across sessions. The proven layout: maintained per-project `index.md` hub, `architecture.md`, `patterns.md`, `guidelines.md`, `debugging.md`, `roadmap.md`, `debt.md`, `thoughts.md`, `adr/-*.md`, `plans/.md`, `journal/.md`. The four previously-drifting consumers now defer to it: `seed/index.md` (pointer), `skills/soul-memory/SKILL.md` (aligned routing + fetch-the-template instruction), `context/session-guidance.md` (aligned routes + pointer), and `commands/soul-context.md` (was pulling the now-removed `plan/tasks.md` → `roadmap.md`). New tests: `tests/seed_doc_test.sh` (5 cases — seeds when absent, no-clobber, no-op on missing seed, bundled seeds exist). No plugin-version bump (one 0.4.0 release). Note: this establishes the "template lives in the store" pattern that #7 reuses for the knowledge system (org template at `mark://root/...`). ### 7. Knowledge-system template + policy, anchored at the `root` hub Every knowledge system gets a guaranteed hub world named **`root`** that tracks things globally (Fritz's call 2026-06-01). The org template + policy live there as ordinary versioned docs: - `mark://root/.well-known/demarkus/template.md` — required structure (same canonical layout as #6). - `mark://root/.well-known/demarkus/policy.md` — strictness level + required tags / rules. Read path is constrained by a hard fact: **hooks cannot authenticate to the broker** (the OAuth token lives in Claude Code's MCP machinery, not a file). So: - The **tag-presence gate works unchanged** on knowledge-system publishes — tags are in the tool-call input the hook receives; no broker access needed. Matcher extends to `mcp____mark_publish` (the per-slug names `/knowledge-join` registers). - The **template/policy are agent-read** (only the agent can reach the broker via the MCP OAuth session). Session guidance: on a knowledge system, fetch `root` policy/template first and follow it. Advisory by necessity, but it is structure convention, not a security boundary. Mirrors the #6 local pattern: a `/project-template.md`-style doc, but hosted on `root`. - For enforced **strictness**, the agent is courier: reads `policy.md`'s declared level → writes `plugin-memory.strictness.` locally → the credential-less hook reads it off disk and enforces. Org declares once on `root`; agent mirrors; hook enforces. Customization = the org edits the two `root` docs. Versioned like any demarkus doc; propagates to every member on next session. No plugin release, no per-user setup. Open question: how the plugin learns which world is `root` — prefer the broker advertising it in `/.well-known` metadata (the join script already hits that endpoint) over a guessed/fixed name. ## Sequencing 1 → 2 → 3 → 6 → 7 → 5 → 4, all stacked on one branch (PR #171) toward a single 0.4.0 release. **Items 1 + 2 + 3 + 6 done.** Next: 7 (knowledge-system template/policy at `root`), then 5, then 4. ## Constraints honored - No core changes — pure plugin composition. - Zero runtime dependencies — pure awk/bash, no jq/python (Fritz 2026-06-01: do not add external deps that may not be installed). - One stacked branch, single 0.4.0 release — no plugin-version churn (Fritz 2026-06-01). - Version pins bumped on every plugin change (`scripts/lib.sh`). - No opt-in capability knob — strictness is a severity level on an always-on check. - Plans live on the soul, not in a vendor folder.