# dsh-agent-notes **English** | [中文](README.md) [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) Turn "why we changed it this way and what we gave up" into **mechanically checked** decision notes inside a repository, delivered as a DeepSeek Harness plugin. > The method is distilled from [czm15053/write-notes-like-deepseek](https://github.com/czm15053/write-notes-like-deepseek), which in turn distills the `.agents/notes/` practice of [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness). > This plugin packages that method as a native DSH plugin: a skill, session switches, real gates, and upstream merging. ## What it solves Code can express *how the system currently runs*; it cannot express **why it must run that way, and what was given up**. So every new session will: "optimize away" an awkward-looking block that was deliberately written that way to avoid a deadlock; punch through global structure to satisfy a local need; and re-propose a design already proven to run out of memory three months ago. This plugin makes the rules **hard to walk around**: same repository as the code, same change, mechanically checked, old decisions sealed with a tamper-evident hash. ## Install ```sh # From a local directory (during development) dsh plugin add --profile /path/to/dsh-agent-notes # Or from GitHub dsh plugin add --profile github:iuuuuuuuu/dsh-agent-notes ``` > **`plugin add` writes the dependency but does NOT enable the plugin.** Verified > behavior: it records the package name in `profiles//package.json` > `dependencies`, but leaves `dsh.profile.bundles` untouched — and DSH loads > plugins **layer by layer from `bundles`** (each layer reads that package's own > `cordis.patch.yml`). So after `add` alone the plugin is installed but never > runs, and `--dump-config` won't show it. > > Add the package name to `bundles` yourself: > > ```jsonc > // $DSH_HOME/profiles//package.json > "dsh": { > "profile": { > "bundles": [ > "@deepseek-ai/dsh-base", > "@deepseek-ai/dsh-web-app", > "dsh-agent-notes" // ← add this line > ] > } > } > ``` > > Then **restart DSH**. To confirm it actually loaded, `dsh --profile --dump-config` > should contain `# == dsh-agent-notes` and `- id: agent-notes`. Present in > dependencies but absent from `bundles` produces **no startup error** while the > plugin silently does nothing — the easiest step to get wrong. ## Usage ``` /agent-notes Show switch and note-tree status /agent-notes on Enable for this session (persisted across reloads) /agent-notes on --global Change the global default (all sessions without an override) /agent-notes reset Clear this session's override, returning to the global default /agent-notes init Scaffold .agents/notes in the current repository /agent-notes gate Run the real gates (also: gate tree / gate format / gate archived) ``` ### Off by default After installing, **both new and existing sessions start with the policy off** — turn it on when you need it (click the chip, or `/agent-notes on`). Installing the plugin therefore never silently changes the behavior of sessions you already have. To enable it everywhere, either: ``` /agent-notes on --global # change the global default at runtime, persisted ``` or set `enabled: true` in the plugin config (only for the profile that carries it). A session override wins over the global default: once you turn a session off, later global changes won't affect it. ### Status indicator in the UI You don't have to type a command to see whether the policy is on for the current session: a green chip "● Agent notes" sits **left of the composer tool row**, next to the permission selector ("Full access" / "Workspace write"), and **clicking it toggles**. It greys out while off. It reads the **session projection** (`agentNotes`), so it stays correct across reloads, reconnects, and session switches. Clicking submits the official slash command (`/agent-notes on|off`), so it is exactly equivalent to typing it, and costs zero tokens. > Design note: the projection only folds the host's own `command/run` events — > **this plugin never writes to the session log**. It cannot: it has zero > `@deepseek-ai` dependencies, so `appendPluginRecord` is unreachable; and the > fallback `session.append('plugin:…')` was measured to write a crippled record > (`ignorable=undefined`, unreadable by `pluginRecordOf`) that looks successful > while polluting the log. Riding the command event sidesteps that entirely. ### Notes board: a "Notes" tab next to Conversation / Trajectory A **"Notes"** tab appears beside Conversation / Trajectory. It hosts the upstream board UI itself (four views: architecture baseline, evolution timeline, rejected- alternatives vault, decision directory) — **no hand-rolled UI**: > The tab iframes the vendored upstream asset > `skills/agent-notes/assets/agent-notes-board.html`; the host only injects the > **current session repository's** notes into the `window.__INLINE_DATA__` slot the > board already provides. > > Why: the upstream board is a mature 76K UI (cross-reference in-degree, causal > chains, multi-dimensional filters). Redrawing it by hand would look worse and drift > from upstream forever. Through an iframe, visuals and interaction follow upstream > updates; this plugin only maintains the data adapter (`buildInlineData`) plus one > prose-rendering patch: upstream `parseMarkdown` has no GFM table rule, and > `lib/board-markdown.js` adds exactly that one rule while every other block keeps the > upstream output verbatim. It reads **live disk content**, not a snapshot: the host registers read-only HTTP routes (`/agent-notes/ui`, `/agent-notes/board`, `/agent-notes/note`, `/agent-notes/health`) and re-reads on every activation. > Why not the session projection: a projection's `view` must be a **synchronous pure > function** over the folded session log — it cannot touch the filesystem (verified in > source: `view: state => wire.view(state)`). > > Security boundary: fixed route paths only, reads confined to `notesRoot` with `..` > rejected, and a session whose notes root cannot be resolved gets **409** — never a > guessed path, never the process working directory. **Two injection hazards are covered by regression tests:** 1. A note body containing `` or ` this repo --(async on plugin startup)--> user machines ``` Neither hop needs anyone to remember. If either fails the job goes red — nothing is skipped silently. **Why `git ls-remote` instead of the GitHub API**: unauthenticated requests hit 403 after 60/hour, which a daily job would inevitably trip. The git protocol has no such limit. **Why resolve a commit instead of anchoring the branch name**: jsDelivr's manifest cache for branch names lags; anchoring a commit yields that commit's real tree. ### When does the model see updated rules `get()` in `lib/skill.js` **reads the disk on every call** — there is no cache. So after the self-update writes new content and calls `invalidate()`, the host drops its cached catalog and the model gets the new content on its next skill load — **DSH does not need a restart**. Caveat: skill content is not injected every turn — it loads on demand (`SkillDefinition` semantics), so a session that already loaded it may still hold the old body. To be certain, re-trigger a skill load, or restart the host. ## Self-check and tests ```sh npm test # behavior tests (real subprocesses, real files) npm run gate:fault-injection # prove the gates can go red rather than being always-green npm run verify # package self-check: manifest / patch / zero-dep / bilingual docs + badge / vendored-vs-upstream digests npm run sync:check # has upstream drifted? bundled copy vs the pinned ref, byte for byte (really goes red) npm run evidence # live evidence against DSH's own real SkillRegistry npm run check # every purely local gate in sequence (run this before pushing) npm run check:full # check + evidence (needs a local DSH) npm run hooks:install # install the commit-msg hook: once per clone, after which a commit that should carry a note is stopped right there npm run archive:note -- # archive and seal (the script computes the hashes — never hand-edit the manifest) npm run doc:budget # documentation size budget (going over goes red and asks you to condense rather than raise the ceiling) ``` `fault-injection` injects, for each gate, the exact defect it should catch (illegal class, bad filename, missing alternatives, a proposal title left in `implemented`, a tampered or deleted sealed archive, …), asserts a non-zero exit, then removes the defect and asserts a return to zero. **If any gate fails to react, the script itself exits non-zero** — and I verified that by injecting a deliberately failing assertion into the script. `hooks:install` deserves its own note: it attaches the "this change should carry a decision note" check to your local `commit-msg`, so you get the feedback at commit time. But a hook can be bypassed with `git commit --no-verify`, so **the CI gate is the authoritative one** — you want both. > `npm run evidence` and the integration tests need to locate a DSH installation. They auto-detect `$DSH_HOME`, `~/.dsh`, and each profile's `node_modules`; for non-standard layouts such as a bundled desktop install, point at it explicitly: > > ```sh > DSH_DIST=/path/to/dsh/lib/bin.js npm run evidence > ``` > > When detection fails, `evidence` **fails loudly (exit code 2)** rather than pretending to pass; the integration tests degrade to a same-contract harness and state which mode they used in their output. ## Design constraint (why zero `@deepseek-ai` dependencies) This plugin **imports no `@deepseek-ai/*` package**: a profile's `node_modules` resolves `@deepseek-ai` only to `cosmokit`/`schemastery`, so a plugin cannot reach the DSH type packages at runtime. Therefore: - host APIs are used **structurally** (`ctx.skills` / `ctx.tools` / `ctx.commands` / `ctx.systemPrompt` / `ctx.agents`), and a missing capability degrades to off instead of crashing; - under `lib/`, only Node built-ins are used (both the browser half and the host half); - tools are registered as **plain objects** (DSH accepts a raw `ToolDefinition`; MCP tools arrive that way). Any `@deepseek-ai/*` import added under `lib/` turns `tools/verify-plugin.mjs` red. The single runtime dependency, `mdast-util-from-markdown`, is used only by the validation scripts under `tools/`, never on the plugin runtime path. ## Components | File | Responsibility | |---|---| | `lib/index.js` | Assembly; mount/unmount of the session-scoped policy | | `lib/state.js` | Global default + per-session override, both persisted (atomic writes) | | `lib/notes.js` | Note-tree discovery, real gate execution, scaffolding | | `lib/skill.js` | The `agent-notes` skill provider (strips frontmatter) | | `lib/policy.js` | The injected policy text | | `lib/commands.js` | The `/agent-notes` command | | `lib/tools.js` | The `agent_notes_gate` / `agent_notes_status` tools | | `skills/agent-notes/` | Vendored upstream content (SKILL.md, references, templates, scripts, assets) | | `tools/` | Upstream merge, self-check, fault injection, tests | ## Known limitations - The **board template references external Google Fonts**, so offline use falls back to system fonts; functionality is unaffected. - Upstream syncing depends on jsDelivr reachability; when unreachable, `sync` fails loudly rather than skipping silently. - DSH is in developer preview. Host API changes may require follow-up. - Session switches persist **by session id**; after a session is deleted its override remains in the state file, and there is currently no GC. - Host-repository CI wiring is left to you; the plugin does not modify repositories. ## License MIT — see [LICENSE](LICENSE) for the full text. The 21 files under `skills/agent-notes/` are a vendored copy of upstream `czm15053/write-notes-like-deepseek` and remain under their authors' copyright. Source, pinned commit, license, and file count are listed item by item in the [third-party notices](THIRD_PARTY_NOTICES.md). --- [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)