cq-guojia/rulemux

rulemux

仓库尚未提供可用的公开描述。

Uncategorized

rulemux

One set of rules, one source of truth: edit once, and it reaches every agent in every workspace. Same effect and same token cost as writing AGENTS.md by hand — never fades out, never accumulates, no symlinks, free to add and remove files.

中文版 | Design docs


1. What is this

1.1 The pain

  • Every agent keeps rules somewhere else: .codebuddy/rules/, .trae/rules/, a single AGENTS.md, an injected recall block… one coding convention has to be rewritten once per agent.
  • And every workspace keeps its own copy: to make a rule apply in a workspace you write it there again.
  • So the rules that are genuinely shared (team conventions, coding standards) can neither be factored out nor maintained: one sentence means opening agents × workspaces files, with no guarantee you did not miss one.

1.2 What rulemux does

You maintain one source of truth: a set of rule documents you wrote, declared in ~/.rulemux/config.toml as the tree below. rulemux sync copies them — really copies — into each agent's native rules directory. Upper layers are inherited by the ones below them, so one edit reaches every agent in every workspace on the next sync.

One source of truth (your own .md files, declared in ~/.rulemux/config.toml)
│
├─ Layer 1 · org-wide rules ─────────────► every agent × every workspace
│   │
│   ├─ Layer 2 · dev rules
│   │   ├─ Layer 3 · mini-app rules
│   │   │   └─ …
│   │   ├─ Layer 3 · backend rules
│   │   │   └─ …
│   │   └─ Layer 3 · client-side rules
│   │       └─ …
│   │
│   └─ Layer 2 · non-dev rules
│       └─ Layer 3 · workspace daily rules …
│           └─ …
│
└─ Layer 4 · one workspace's own rules ──► only in that workspace

              rulemux sync  ↓  real copies (never fades out / never accumulates / no symlinks)

    workspace A ──► .codebuddy/rules/   .trae/rules/        (dsh: injected, nothing on disk)
    workspace B ──► .codebuddy/rules/   .trae/rules/        (dsh: injected, nothing on disk)
  • Rules are reused: layer 3 inherits layer 2, layer 2 inherits layer 1 — write what is shared once, and get more specific as you go down.
  • Layers fan out: each layer declares which agents and which workspaces it applies to; no file is duplicated per combination.
  • Edit once, applies everywhere: change a sentence up top and every workspace and agent that inherits it is aligned on the next sync.
  • No depth limit: the … under layer 3 is a padding layer — use is expanded recursively, so you can nest as deep as you like. The only thing not allowed is a cycle (A uses B and B uses A), which fails at config load.

The tree is built with use on [[file_group]] (inherit the layer above) plus workspace on [[source]] (where it lands):

[[file_group]]
name = "base"                    # layer 1: org-wide
path = ["/rules/00-base.md"]

[[file_group]]
name = "dev"                     # layer 2: dev rules
use  = ["base"]                  # inherits layer 1
path = ["/rules/10-dev.md"]

[[file_group]]
name = "miniapp"                 # layer 3: mini-app rules
use  = ["dev"]                   # inherits layer 2 (and therefore layer 1)
path = ["/rules/20-miniapp.md"]

[[source]]
groups    = ["miniapp"]          # lands as base + dev + miniapp
workspace = ["/work/miniapp-a", "/work/miniapp-b"]

[[source]]
path      = ["/rules/proj-x-only.md"]   # layer 4: this workspace only
workspace = ["/work/proj-x"]

See §5.2 for the full grouping and matching syntax.

1.3 The invariants

  • rulemux does not own or maintain any rule content: your source files stay yours — rulemux only delivers them. Which agents / workspaces they apply to is declared in your config.
  • The session hook is (almost) only the courier. Rule content is never injected: files are really copied into the directory the agent loads natively, so they enjoy static-prefix semantics (never fade out mid-conversation, never accumulate). The single, deliberate exception is one transient notice line the hook emits when it detects that the rules really changed — it asks you to start a new session. See §5.3.
  • No symlinks, free add/remove: real copies only; delete a source from the config and its copy disappears on the next sync.

Why not "just inject with a hook"? Hook injection lands in the dynamic part of the context: it gets summarised away on compaction (fades out) or re-appended every turn (token blow-up). See design/architecture.md §一.


2. Supported scope

rulemux ships one adapter per agent, and an adapter is only installable once its rules directory and hook location have been confirmed by a real canary test. Today:

AgentTierRules directorySkills directoryStatus
codebuddyTier-1 (real copy).codebuddy/rules/; user-level ~/.codebuddy/rules/.codebuddy/skills/; user-level ~/.codebuddy/skills/ + ~/.workbuddy/skills/ (both written)✅ Verified — installable (codebuddy-cn is an alias)
workbuddyTier-1 (real copy).codebuddy/rules/ (shared with codebuddy, canary-proven 2026-10-09).codebuddy/skills/ (shared with codebuddy); user-level: both dirs written as above✅ Verified — installable (separate app: own ~/.workbuddy/settings.json hook, no longer an alias of codebuddy)
claude (Claude Code)Tier-1.claude/rules/not yet⚠️ registered, not verified yet — cannot be installed
trae (Trae CN)Tier-1 (real copy).trae/rules/.trae/skills/ (workspace, 🔴 pending canary); user-level ~/.trae-cn/skills/ (verified)✅ Verified — installable (2026-10-09 canary: user-level hook ~/.trae-cn/hooks.json; the international ~/.trae build is not wired yet)
codexTier-2 (injection)none — injects into contextnot yet⚠️ registered, not verified yet
opencodeTier-2 (injection)none — injects into contextnot yet⚠️ registered, not verified yet
dsh (DeepSeek Harness)Tier-2 (injection)none — injects into context.dsh/skills/; user-level ~/.dsh/skills/✅ Verified — installable; rules are delivered by a separate dsh plugin — install it with dsh plugin --profile <p> add rulemux-dsh (rulemux only renders the text with inject, and writes nothing into your workspace for rules). Skills are real copies into the native skills dirs, synced by the same plugin at session start (source-verified 2026-10-14 against deepseek-ai/deepseek-harness, verified on a real dsh 2026-10-15)
  • rulemux init refuses any agent that is not verified — it will not half-install an adapter whose behaviour has not been proven. rulemux doctor marks unverified agents with ⚠.
  • Skill injection (shipped 2026-10-12, codebuddy / workbuddy; dsh added 2026-10-14): declare local skill directories with [[skill_source]]; they are really copied into the agent's native skills directory (as __rulemux__<skill-name>/ — your own skills are never touched). With scope = "user" they land in the user-level (global) directories. Rules and skills both support the dual scope; see §5.4. claude / codex / opencode do not take skills yet; trae takes skills (CN: workspace .trae/skills + user-level ~/.trae-cn/skills, workspace pending canary).
  • Tier-2 is a deliberate downgrade for agents that have no rules directory (they only read a single AGENTS.md). It injects via the session hook and never touches your own AGENTS.md, but it cannot satisfy the "never fades out" bar. See design/features/hook-injection.md.
  • dsh is plugin-delivered: for rules nothing lands on disk, for skills real copies land in .dsh/skills/: it has no hook file and no rules directory, so rulemux never writes into your workspace for rules. A separate plugin package in this repo (dsh-plugin/) — on npm as rulemux-dsh — asks the CLI for the rules rendered for the session's workspace (rulemux inject --agent dsh) and injects that text once, on the first user turn. Install it the normal dsh way — dsh plugin --profile web add rulemux-dsh (or straight from git: add "github:cq-guojia/rulemux#path:/dsh-plugin"); rulemux init --agent dsh installs nothing — it just prints that command. Skills (2026-10-14): dsh does have a native skills subsystem (.dsh/skills in the workspace, ~/.dsh/skills user-level — both source-verified against deepseek-ai/deepseek-harness), so the same plugin also runs rulemux sync --hook --agent dsh when a session starts and real copies land there as __rulemux__<skill-name>/. Nothing else is written, and .agents/skills (a cross-framework convention dir) is never touched. The plugin then sets itself up as dsh starts (restart dsh after installing): readiness is three steps and all of them must hold — the rulemux CLI is resolved and its version satisfies the plugin's minimum (an older one is upgraded with pnpm/npm and re-checked — never accepted as-is), the plugin itself is loaded, and ~/.rulemux/config.toml exists (created for you when missing, never overwritten). Any step that does not hold raises, so the session fails visibly instead of quietly running without rules. Because it happens at startup, ~/.rulemux/config.toml is already there when you go to edit it: install → restart → edit the config → open one session. Asking for the rules is a separate concern: a failing rulemux inject is logged, and the session carries on without an injected block. Uninstalling rulemux cannot stop the plugin (it belongs to the host), so rulemux uninstall --agent dsh says so — and points out any leftover .dsh/rules files from the old design, which rulemux no longer writes but also no longer deletes.

3. Install

rulemux is a single Go binary with zero third-party dependencies.

# a) from a GitHub Release (recommended): put the binary on your PATH
#    e.g. /usr/local/bin/rulemux   (Windows: rulemux.exe)

# b) from source
go install github.com/cq-guojia/rulemux@latest
# or
git clone https://github.com/cq-guojia/rulemux.git && cd rulemux && go build -o rulemux .

# c) via npm (convenience channel that puts the binary on your PATH)
npm i -g rulemux

There is no auto-update. Upgrades are manual and handled by your package manager (go install …@latest, Homebrew/Scoop/apt later on). This is a deliberate decision — see design/requirements.md §五.

An upgrade keeps your hooks current — by itself, with no npm install scripts. A hook command can change between releases (0.2.0 added --hook), so rulemux fixes its own stale hook the next time an agent runs it: the first rulemux sync after an upgrade rewrites rulemux's own SessionStart entry into the current format. Nothing else is touched — other tools' hooks and every other key stay as they were, and nothing is ever created. Because this lives in the binary, it works no matter how rulemux was installed and needs no postinstall allow-list from npm.

You can also do it on demand:

rulemux init --refresh   # rewrite rulemux's OWN hooks only: never installs, never creates files

rulemux doctor now reports a hook that is installed but out of date (and how to fix it). If you moved this agent's config directory with CODEBUDDY_CONFIG_DIR, rulemux follows it — see design/external/agent-rules-dirs.md §五.

3.1 dsh: a plugin, not a hook

Every other agent is “rulemux init --agent <id> installs a session hook”. dsh is the exception: it has no hook file and no rules directory, so rulemux init --agent dsh installs nothing and only prints the command below — a dsh plugin does the delivery, asking rulemux for the text:

dsh plugin --profile <your profile> add rulemux-dsh        # npm
# or straight from git, to try an unreleased change:
dsh plugin --profile <your profile> add "github:cq-guojia/rulemux#path:/dsh-plugin"

Restart dsh after installing: the plugin becomes ready when dsh starts — it checks the rulemux CLI (upgrading it and re-checking when it is too old) and creates ~/.rulemux/config.toml when it is missing (never overwriting one that exists). Any step that fails raises, so a session never quietly runs without rules. Because this happens at startup, the config file is already there when you go to edit it: install → restart → edit the config → open one session.

On that session the plugin runs rulemux inject --agent dsh and injects the rendered rules — nothing is written into your workspace: no .dsh/rules, no copies, nothing left behind to clean up. (Older versions of rulemux wrote copies into .dsh/rules and the plugin read them back; that directory is now unused — if you have one left over, delete it yourself, or run rulemux uninstall --agent dsh which will point at it.)


4. Quick start

# 1. Install the session hook for an agent (--agent is REQUIRED)
rulemux init --agent workbuddy
#    First run also writes a sample config to ~/.rulemux/config.toml

# 2. Edit ~/.rulemux/config.toml and point `path` at your real rule files

rulemux doctor    # 3. Self-check: binary/PATH, agents, config validity
rulemux verify    # 4. Canary check: drop a probe and ask the agent to recite its token

# From now on every new session triggers `rulemux sync --hook --agent codebuddy` automatically.

rulemux sync --all   # optional: re-align EVERY workspace recorded in the ledger, in one shot

rulemux uninstall --agent codebuddy   # remove rulemux again (--yes skips the confirmation)

Where the hook goes: into the user-level host config ~/.codebuddy/settings.json (the same place a tool like Hindsight registers itself). Install once, and it applies to every workspace — you do not re-run init per project. When it fires, rulemux uses the current workspace (its cwd) to match the workspace entries in your config and decides what to deliver.


5. Configuration

Config file: ~/.rulemux/config.toml (override with --config <path> on any command).

5.1 Each [[source]]

FieldMeaning
pathSource file(s), anywhere on disk. Single value or a list. A list shares the agents/workspace below.
agentsWhich agents receive this batch. Omitted = all supported agents.
workspaceWhich workspaces these rules apply to. Omitted / "*" / "**" / "all" = every workspace. Single value, a list, or a glob.
scopeWhere the batch lands: "workspace" (default) = each workspace's agent dir; "user" = the agent's user-level (global) dir, shared by every workspace. With scope = "user", workspace is ignored.

Globbing follows the usual rules: * is one path segment, ** spans segments and may appear in the middle — e.g. workspace = "/abs/**/B" matches a directory named B at any depth.

[[source]]
path = ["/your/rules/a.md", "/your/rules/b.md"]
agents = ["codebuddy"]              # omit to target every supported agent
# workspace is omitted => every workspace
# workspace = ["/path/to/proj-a", "/path/to/proj-b"]
# workspace = "/abs/**/B"           # glob: any depth, named B
# scope = "user"                    # land in the agent's user-level (global) dir instead

5.2 Bundling with "groups" (optional)

When you have many files, bundle them and reference the bundle by name.

# ---- File groups ---------------------------------------------------------
[[file_group]]
name  = "base"
path  = ["/your/rules/team-conventions.md", "/your/rules/style.md"]

[[file_group]]
name = "proj"
use  = ["base"]                     # nests the base group (a list: ["base", "dev"])
path = ["/your/rules/project-a.md"]

# ---- Workspace groups ----------------------------------------------------
[[workspace_group]]
name      = "dev"
workspace = ["/path/to/proj-1", "/path/to/proj-2"]

# ---- Use them ------------------------------------------------------------
[[source]]
groups           = ["base", "proj"]      # expands to every file in those groups
path             = ["/your/rules/extra.md"]   # mixing is allowed: groups + standalone files
agents           = ["codebuddy"]
workspace_groups = ["dev"]               # expands to every workspace in the group
workspace        = ["/path/to/standalone"]    # also allowed alongside workspace_groups

Things worth knowing:

  • use is a list — use = ["dev", "qa"] reuses several groups at once, and groups nest (A pulls in B plus its own files).
  • There is no depth limit — use is expanded recursively (for file groups and workspace groups alike), so you can nest as deep as you like. The only thing not allowed is a cycle (A uses B and B uses A), which fails at config load.
  • Mixing is allowed: groups with path, and workspace_groups with workspace, in the same source. The result is the union.
  • Duplicates are harmless — everything is deduplicated by value, so each file is processed once.
  • File groups and workspace groups are separate namespaces; the same name may be used in both.

5.3 What sync does, and what it never touches

On every sync rulemux computes the set of prefixed files that should exist, then removes any __rulemux__* file that is not in that set, copies/overwrites the ones that are missing or changed, and skips the rest.

  • Destination name: __rulemux__ + source basename (a short hash is appended on name collisions).
  • Your source files are only ever read — never modified or deleted.
  • Files without the __rulemux__ prefix in the rules directory are never touched, so your own rule files stay safe.

⚠️ Run rulemux sync after editing your config or source docs After you edit ~/.rulemux/config.toml (adding/removing a [[source]], changing path) or any source rule document, run rulemux sync --agent codebuddy (a bare rulemux sync only touches the agents already recorded for this workspace — name --agent <id> to add one).

  • If you run sync: the next new session reads the updated rules.
  • If you don't: CodeBuddy / WorkBuddy still auto-sync on every session start via the SessionStart hook, but the harness fixes the rule snapshot at the start of the session (before the hook runs), so the current session still sees the old version — the update only takes effect on the second new session.

5.4 Skill injection ([[skill_source]], shipped 2026-10-12)

A skill is a directory (SKILL.md plus optional references/ / scripts/), loaded progressively (name+description at session start, full text on demand) — so, exactly like rules, it can only be delivered by a real copy into the native skills directory, never by hook injection. Add [[skill_source]] to ~/.rulemux/config.toml:

[[skill_source]]
path = ["~/my-skills"]              # skill root: every child dir containing SKILL.md is one skill
agents = ["codebuddy"]              # omitted = every supported agent
workspace = "*"                     # omitted = every workspace (workspace scope only)
# groups = ["frontend"]             # reuse a [[skill_group]] bundle instead of repeating path
# workspace_groups = ["dev"]        # reference a [[workspace_group]] bundle (shared with rules)

[[skill_source]]
groups = ["frontend"]               # bundle several skill roots under one name
agents = ["codebuddy"]
scope = "user"                      # user-level (global): workspace-independent, every workspace

[[skill_group]]                     # bundle skill roots so several sources can reuse them
name = "frontend"
path = ["~/react-skills", "~/css-skills"]
# use = ["base"]                    # nest another skill group (list, may be multiple)
  • Skill sources take the same fields as rules: path, agents, workspace, scope, plus the bundling keys groups (→ [[skill_group]]) and workspace_groups (→ [[workspace_group]]). [[skill_group]] bundles several skill root directories (name / path / use); nesting and cycle detection work exactly like file groups.
  • Where they land: workspace scope → the agent's native skills dir — <workspace>/.codebuddy/skills/ (shared by codebuddy and workbuddy) or <git根>/.dsh/skills/ (dsh) or <workspace>/.trae/skills/ (trae, CN, 🔴 pending canary); user scope → ~/.codebuddy/skills/ and ~/.workbuddy/skills/ (both written, so both apps see it) or ~/.dsh/skills/ (dsh) or ~/.trae-cn/skills/ (trae, CN). Rules work the same way: [[source]] also accepts scope = "user", landing in ~/.codebuddy/rules/ + ~/.workbuddy/rules/.
  • Same discipline as rules: the target is __rulemux__<skill-name>/; your own skills (no prefix) are never touched; removing a source removes its copies on the next sync; sources are only ever read.
  • Remote / market sources (path takes more than a local dir): gh:owner/repo or a full git URL (git@host:owner/repo.git = SSH, https://… = HTTPS) is git cloned into ~/.rulemux/skills/<owner>-<repo>/ on the first sync; npm:<package> is npm packed and unpacked there. After the first fetch it is treated as a local dir (zero network on later syncs). Transport (SSH vs HTTPS) is whatever the URL you wrote implies — rulemux never picks for you. Skills under the source root are auto-discovered (bounded scan for SKILL.md, like npx skills --all), so a repo that groups many skills (e.g. gh:anthropics/skills) works with no extra config; append #subpath (e.g. gh:anthropics/skills#skills/docx) to point at one container or a single skill. Inspect with rulemux skill list; upgrade with rulemux skill update <key> / --all (git pull / npm re-pack, in place — manual only, never in the hook). Top-level fetch_timeout = "30s" caps how long a first-time fetch may run before it warns and skips on the hook path.
  • claude / codex / opencode do not take skills yet; trae takes skills (CN).
  • Skill add/change does not trigger the "start a new session" notice (hot-reload is still being verified); rule behaviour is unchanged.
  • dsh skill frontmatter quirks (2026-10-14, source-verified): dsh requires the frontmatter name to match ^[a-z0-9]+(?:-[a-z0-9]+)*$ and rejects the legacy camelCase invocation keys (disableModelInvocation / modelInvocable / userInvocable) by dropping the whole skill — CodeBuddy accepts both. rulemux copies skills byte-for-byte and never rewrites content, so a skill that loads in CodeBuddy may silently not load in dsh. CodeBuddy marketplace extras (display_name / description_zh / version / allowed-tools) are simply ignored by dsh. dsh also finds project skills relative to the git root (nearest .git), not your cwd.

5.5 The complete default config (annotated)

rulemux init writes this file to ~/.rulemux/config.toml on first run. Everything is commented out — nothing is synced until you uncomment an example or add your own with real paths. It is reproduced here one-to-one so you can read the whole thing before editing your copy.

# rulemux configuration file
#
# Default location: ~/.rulemux/config.toml (or pass --config <path> to any command)
#
# At the start of every agent session, rulemux copies the "source rule files" described
# by each [[source]] below into that agent's native rules directory, using a __rulemux__
# prefix: it cleans up its own residue and never touches your own files.
#
# Each [[source]] accepts these fields (all optional, each has a default):
#
#   path      Source file(s); may live anywhere on disk. Both forms work:
#               path = "/abs/path/to/your-rules.md"          # a singl