# 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. The publish gate uses a broad `mcp__.*__mark_publish` matcher and scopes precisely in-script (local soul + registered knowledge systems only). 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). 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 — DONE (plugin side) 2026-06-01 (PR #171) Every knowledge system gets a guaranteed hub world named **`root`** that tracks things globally. `root` discovery resolved as a **fixed-name convention** (Fritz's call); broker-advertised discovery deferred as a future enhancement. Plugin side built + tested: - **Gate extends to joined knowledge systems.** The publish-gate matcher is broadened to `mcp__.*__mark_publish`, and `publish_gate_scope` (lib.sh) classifies each call: `local` (server contains `demarkus-memory` → global strictness), `ks:` (a registered knowledge system → per-slug strictness), or empty (a demarkus server the plugin doesn't manage → not gated). So the tag check now enforces on org writes too, but never on an unrelated personal soul. - **Registry.** `/knowledge-join` runs `scripts/register-knowledge.sh ` after `claude mcp add`; `register_knowledge_system` / `is_registered_knowledge_system` (lib.sh) maintain `~/.demarkus/knowledge-systems` (one slug per line, idempotent). - **Per-slug strictness.** `configured_strictness [slug]` precedence: env → `plugin-memory.strictness.` → global `plugin-memory.strictness` → warn. The agent mirrors the org's declared level here (courier). - **Convention + reference docs.** `examples/knowledge-system/` (README + `policy.md`) documents the `root` well-known paths (`mark://root/.well-known/demarkus/{policy,template}.md`), the policy format (a `strictness:` line + required tags), and the agent-courier flow. `commands/knowledge-join.md` updated: register the slug, then fetch+follow the `root` template/policy and mirror strictness. - Tests: `tests/knowledge-gate_test.sh`, 8 cases (registry idempotency, local still gated, foreign/unregistered ignored, registered gated, per-slug strictness + isolation, tagged defers). Deferred / out of plugin scope (org + deployment side): actually creating the `root` world and publishing `policy.md`/`template.md` on a live knowledge system (e.g. `knowledge.demarkus.io`); broker advertising the hub world in `/.well-known` metadata; de-registration on `claude mcp remove` (stale entry is harmless — never matched). The agent-read of `root` docs is advisory by necessity (only the agent can reach the broker; a hook cannot). ## 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 + 7 (plugin side) done.** Remaining: 5 (`/soul-doctor` hygiene tooling), 4 (recall nudge). Org-side `root` setup tracked separately from the plugin PR. ## 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. --- ## Update 2026-06-01: `root` hub is live (no longer hypothetical) Fritz stood up the `root` hub on the `knowledge.demarkus.io` knowledge system, implementing the #7 convention exactly: - `mark://root/index.md` — hub listing worlds (`world-a`) + discovery + links to the two well-known docs. - `mark://root/.well-known/demarkus/policy.md` — `strictness: warn` + required-metadata prose. Parses cleanly for the plugin's agent-courier (mirror the `strictness:` line → `plugin-memory.strictness.`). - `mark://root/.well-known/demarkus/template.md` — per-project layout mirroring `seed/project-template.md`. So item 7's deferred "org-side" piece is now done by Fritz, and the plugin convention is validated against a real system. The live policy is minimal (strictness + required-metadata); the richer policy ideas from the design discussion (publish bar / what-belongs-here, taxonomy axes via `axis:value` tags, owner + policy_version metadata, and optionally hook-enforced `require_tags`) are NOT yet on the live policy — open question whether to enrich it. Slug consistency is a non-issue: the join flow uses one slug throughout (`claude mcp add `, registry, and strictness file all keyed the same), so the gate keys correctly regardless of what the derived slug is. --- ## Update 2026-06-01 (cont.): require_tags enforcement, live policy, and #5 done - **`require_tags` (extends #7).** The publish gate now presence-checks tag axes a knowledge system declares. `configured_require_tags [slug]` (lib.sh) reads `plugin-memory.require-tags.`; `publish_metadata_check` emits the decoded tags as a 6th line; the gate flags any required axis lacking an `axis:value` token, folded into the same warn/block/ask flow. 6 new `knowledge-gate` tests (13 total). - **Live `root` policy enriched (org-side of #7, done with Fritz).** `mark://root/.well-known/demarkus/policy.md` v2: `strictness: block`, `require_tags: category`, `owner: fritz@latebit.io`, publish-bar + `axis:value` taxonomy. `category:` is the required axis; `team:/type:/status:` documented but not enforced (deferred until multiple teams write). Policy doc self-tagged `category:governance`. Plugin example/README/knowledge-join.md aligned to `category`. - **#5 `/soul-doctor` DONE.** New read-only audit command (`commands/soul-doctor.md`): core checks from one `mark_graph` (depth 5) + `mark_list` crawl — broken links (cross-referenced against the inventory, since a target uncrawled-by-depth is NOT broken; verified with a fetch), orphans (no inbound edge, not the root hub), stale index entries, missing per-project hub, untitled docs, ADR sequence gaps. Optional deep checks (per-doc fetch): untagged docs, duplicate content (content-hash), knowledge-system taxonomy vs `root` policy. Agent-driven via MCP (works on local soul + knowledge-system worlds); never writes. Dogfooded against demarkus-soul: found a real orphan (`/plans/information-graph.md` not linked from the index) and corrected a depth-trap bug in the command's own broken-link example. **Remaining: only #4 (recall-before-answering nudge).** Then the plugin work for this PR is complete. --- ## Update 2026-06-01 (final): #4 done — plan complete - **#4 recall-before-answering nudge — DONE.** `hooks/recall-nudge.sh` (UserPromptSubmit hook). When the prompt matches a tight recall-intent pattern (`did we`, `what did we`, `have we`, `last time`, `do we have a note/adr`, `we decided/already`, `recall`, ...), it injects a discreet non-blocking `additionalContext` reminder to `mark_lookup` the project + fetch its index before answering. Gated on the plugin being configured. Pure grep/tr (only the `prompt` field carries prose, so no JSON scanner needed). UserPromptSubmit contract verified against the docs (`prompt` on stdin; `additionalContext` injects; not blocking). Tests: `tests/recall-nudge_test.sh`, 5 cases (recall phrases nudge, ordinary prompts don't, case-insensitive, unconfigured→silent). Noise risk flagged as with the journal nudge: a disable knob is deferred until there's signal it's needed. - `session-guidance.md` reconciled: recall now has a soft backstop too; only routing-to-the-right-file is fully unbacked. **All seven items done. Plan complete.** The demarkus-memory plugin v0.4.0 (PR #171) moves from advisory to enforced: publish tag-gate (warn/block/ask + per-knowledge-system strictness & `require_tags`), session-end journal nudge, recall nudge, canonical `/project-template.md`, knowledge-system policy/template at the live `root` hub, and the `/soul-doctor` hygiene audit. Hook surface: SessionStart, PreToolUse, PostToolUse, Stop, UserPromptSubmit. Test suites: publish-gate (22), knowledge-gate (13), session-journal-nudge (11), recall-nudge (5), seed_doc (5), knowledge-join (11) — 67 total, all pure awk/bash, zero runtime deps. Remaining tail (not blocking): wire the plugin shell tests into CI; optional disable knobs for the two nudges if they prove noisy. --- ## MERGED 2026-06-01 PR #171 merged to main. demarkus-memory v0.4.0 shipped — all seven items complete. This plan is **closed**; see the journal entry for 2026-06-01 for the wrap-up and the CodeRabbit-review lessons. Tail (separate, unstarted): plugin shell tests → CI; optional nudge disable knobs. ## Related documents - [Claude Code plugin](/plans/claude-code-plugin.md): the plugin this plan hardens - [LOOKUP verb](/plans/lookup-verb.md): the catalog lookup the tag gate protects