**CHORUS** is shared **group memory** for Claude / CoWork sessions: one Obsidian vault serving a whole team, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). Every member's sessions read the same knowledge graph and write to it with full attribution — where ECHO reflected one voice back, CHORUS is many voices in one performance.
Architected by **Jason Stedwell**. CHORUS is a group-based fork of [ECHO](https://git.alwisp.com/jason/echo) (the single-operator original, preserved at the `echo-fork-point` tag) and a **clean break** from it: fresh vaults on fresh machines, no ECHO back-compat. The conversion plan and settled design decisions live in [CHORUS-PLAN.md](CHORUS-PLAN.md); deployment endpoint: `https://chorusapi.mpm.to`.
The skill makes direct REST calls through a bundled validated client (`scripts/chorus.py`). The whole toolchain is **pure Python** (stdlib only), so it runs identically on Windows, macOS, and Linux — no bash, no platform-specific `date`.
This repository (`jason/chorus`) holds the plugin **source** (tracked tree at `chorus-memory.plugin.src/`), the built `chorus-memory.plugin` artifact (rebuilt on each version bump; versioned builds ship as Gitea releases), and a credential-free `eval/` test harness.
One vault, many members. Each machine is configured with the **group** and its own **member id** (a kebab-case slug like `jason`), and every agent write is stamped `author: <member>`.
- **Shared by the group:** projects, areas, resources, decisions, the journal (one team timeline — every `## Agent Log` line is member-tagged `- YYYY-MM-DD [member]: …`), semantic/episodic/working memory including `group-profile.md` (what the team is + a `## Members` roster), a shared group inbox for unowned items, the entity/recall indexes, and the duplicate gate (two members can't create parallel notes for the same client — a blocked capture names whose entity it collided with).
- **Per member, no contention by construction:** `_agent/members/<member>/{preferences,current-context,heartbeat,inbox}.md` and `_agent/sessions/<member>/` — each member's profile, active scope, last-session pointer, personal captures, and session logs are theirs alone.
- **Self-onboarding:** a new member installs their plugin and runs a session — `load` detects the missing namespace, seeds it, and adds them to the roster.
- **Group lenses:** `recall --author <member>` filters the graph to one member's notes; `load --all-members` shows every member's scope and last session.
- **Visibility is by design:** one vault, one key — everything in it (including member preferences) is readable by the whole group. Truly private material belongs in a personal vault, not CHORUS.
Namespacing removes most contention outright. What stays shared-write is built for interleaving: member-tagged whole-line-idempotent appends for logs/inboxes, and the machine indexes are guarded by an advisory lock with **bounded retry/backoff** (`CHORUS_LOCK_RETRIES`, default 4) plus a fresh re-read inside the critical section, so concurrent captures merge instead of clobber. Lock owner ids carry the member (`<member>-<client>-<pid>`), making contention attributable. Full model: `skills/chorus-memory/references/operating-contract.md`.
The plugin ships **no** group, member, endpoint, or key — each machine supplies them via a machine-local JSON config (resolution in `scripts/chorus_config.py`):
- **Set up a machine:** `chorus.py config import <file>` adopts a config you've been handed; `chorus.py config set --group … --member … --endpoint … --key …` writes one from values; `config init` scaffolds a template; `chorus.py config` prints the resolved config (key redacted) with per-field sources.
- **First run:** an unconfigured machine reports `NOT CONFIGURED` (`load` exits `78`; `/chorus-doctor` flags it red) and the skill asks the member for their config before doing memory work. The config is never committed and never written into a vault note.
The CoWork sandbox doesn't bridge your real `~/.claude`, but the plugin itself is mounted every session — so each member's credentials travel *inside* their artifact:
A *complete* baked set (endpoint + key + member) is **authoritative** — it can't be shadowed by stale env vars or leftover configs. The committed source ships zero credentials; baked artifacts land in `dist/` (gitignored) and are delivered directly to their one member, **never committed or published**. The full member-onboarding runbook is [docs/ONBOARDING.md](docs/ONBOARDING.md).
**Division of responsibility:**`SKILL.md` owns day-to-day procedure; `references/operating-contract.md` owns durable principles, safety rules, and the N-writer concurrency model; `scripts/routing.json` is the machine-readable source of truth for what may be written where (`references/routing-map.md` is its human authority; `check_routing.py` keeps them in sync mechanically).
The complete map — every write destination with its trigger and why it's distinct — is `skills/chorus-memory/references/routing-map.md`. Frontmatter conventions (incl. the required `author:` on agent-written notes) are in `references/vault-layout.md`.
Slash commands: `/chorus-load`, `/chorus-save`, `/chorus-recall`, `/chorus-triage`, `/chorus-health`, `/chorus-sweep`, `/chorus-reflect`, `/chorus-doctor`. Session hooks auto-load memory at SessionStart and nudge reflection once per substantive session at Stop.
| **2.0.0-rc.1** | Phase 5: full group-voice docs, member onboarding runbook, CHORUS icon. Plan complete — pending live deployment at `chorusapi.mpm.to`. |
| **2.0.0-alpha.1** | Phase 1: mechanical echo→chorus rename; clean break from ECHO (no back-compat). |
The pre-fork ECHO history (v0.3 → v1.5.1) lives in the [`jason/echo`](https://git.alwisp.com/jason/echo) repo and at this repo's `echo-fork-point` tag.