The plugin upload validates each skills/**/SKILL.md `description` frontmatter against a 1024-char cap (distinct from plugin.json's <500 rule). The skill description was 1171 chars and got rejected on upload. - Trim the chorus-memory SKILL.md description to 1000 chars, preserving the save/recall/group triggers and the full "Do NOT" exclusion list. - Add MAX_SKILL_DESCRIPTION=1024 guard to build.py: scans every SKILL.md and fails the build with an over-by-N message so this can't silently regress. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chorus-memory — v2.0.0-rc.1
CHORUS is shared group memory for Claude / CoWork sessions: one Obsidian vault serving a whole team, driven over the 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 (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; 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.
The group model (schema 5)
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 Logline is member-tagged- YYYY-MM-DD [member]: …), semantic/episodic/working memory includinggroup-profile.md(what the team is + a## Membersroster), 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}.mdand_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 —
loaddetects 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-membersshows 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.
Concurrency posture
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.
Configuration
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):
- File:
~/.claude/chorus-memory/config.json— honors$CLAUDE_CONFIG_DIR; path overridable with$CHORUS_CONFIG. Shape:{ "group": …, "member": "<kebab-slug>", "endpoint": "https://…", "key": "…" }.memberis required — it attributes every write. - Per-field env override:
CHORUS_GROUP,CHORUS_MEMBER,CHORUS_BASE(endpoint),CHORUS_KEY. - 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 initscaffolds a template;chorus.py configprints the resolved config (key redacted) with per-field sources. - First run: an unconfigured machine reports
NOT CONFIGURED(loadexits78;/chorus-doctorflags 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.
Per-member baked builds — the onboarding mechanism
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:
python build.py --bake-key --from alice-config.json --label alice
# -> dist/chorus-memory-2.0.0-rc.1-alice.plugin (carries alice's member id + the vault key)
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.
Requirements
- Python 3 in the session environment (stdlib only;
python3, orpython/py -3on Windows). - Obsidian running on the backend with the Local REST API plugin enabled.
- HTTPS reachability to
https://chorusapi.mpm.tofrom the Claude / CoWork session environment.
Repository layout
chorus/
├── README.md · CHANGELOG.md · CHORUS-PLAN.md ← this file · release log · conversion plan + decisions
├── docs/ ← ONBOARDING.md (member runbook) · USAGE.md · history/
├── build.py ← deterministic .plugin builder (--bake-key/--strip-key/--label/--outdir)
├── chorus-memory.plugin ← built, installable plugin (current pointer; versioned builds → Gitea releases)
├── chorus-memory.config.template.json ← blank config template (group/member/endpoint/key)
├── dist/ ← per-member baked artifacts (secret-bearing) — GITIGNORED, never committed
├── eval/ ← credential-free test harness; not bundled
│ ├── mock_olrapi.py · mock_olrapi_hifi.py ← deterministic REST-API mocks (+ fault injection / real PATCH semantics)
│ ├── run_eval.py ← retrieval / dedup / write-safety / durability metrics
│ ├── test_features.py · test_reflect.py · test_offline_queue.py · test_patch_semantics.py
│ ├── test_multimember.py ← 26-check two-member end-to-end suite (the group behavior)
│ └── results/latest.json
└── chorus-memory.plugin.src/ ← tracked source tree (the plugin — single canonical tree)
├── .claude-plugin/plugin.json ← manifest (name, version, description)
├── README.md ← plugin-level README (configuration, tooling)
├── hooks/hooks.json ← SessionStart auto-load · Stop reflection nudge
├── commands/ ← /chorus-{load,save,recall,triage,health,sweep,reflect,doctor}
└── skills/chorus-memory/
├── SKILL.md ← operating procedure (authoritative)
├── references/ ← operating-contract · bootstrap · vault-layout · routing-map · api-reference
├── scripts/ ← pure-Python toolchain (chorus.py client, bootstrap/migrate/sweep,
│ vault_lint, routing.json manifest, capture/recall/triage/reflect ops)
└── scaffold/ ← verbatim seeds bootstrap writes into the vault
(group-profile, group-inbox, member-* anchors, templates, marker)
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).
Vault layout (data only, root-addressed)
/vault/
├── inbox/ ← captures/group.md (unowned, member-tagged) · imports/ · processing-log/
├── journal/ ← daily/ (shared team timeline, member-tagged Agent Log) · weekly/ monthly/ quarterly/ annual/
├── projects/ ← active/ incubating/ on-hold/ archived/ (folder == status:)
├── areas/ ← business/ personal/ learning/ systems/
├── resources/ ← people/ companies/ concepts/ references/ meetings/
├── decisions/ ← by-date/ (ADR-style)
└── _agent/
├── chorus-vault.md ← bootstrap marker (schema_version: 5, plugin-owned)
├── members/<member>/ ← preferences.md · current-context.md · heartbeat.md · inbox.md (per member)
├── sessions/<member>/ ← YYYY-MM-DD-HHMM-<slug>.md (member-namespaced session logs)
├── memory/ ← working/ · episodic/ · semantic/ (incl. group-profile.md)
├── context/ health/ templates/ skills/ index/ locks/
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.
Bundled tooling & commands
scripts/chorus.py is the validated client: keep-alive connection-pooled, HTTP-status-checked (failed writes exit non-zero), retrying, read-back-verified PUTs, whole-line idempotent appends. High-level ops collapse the write discipline into one call: capture (route + complete frontmatter incl. author: + index + auto-link + member-tagged log line, with a pre-write duplicate gate), resolve, recall [--author <member>] (hybrid BM25 + graph over the shared corpus), link, triage (both inboxes, attributed audit trail), load [--all-members] (8-read cold start + self-onboarding), plus scope, lock/unlock, config, doctor, and the vault maintenance scripts (bootstrap.py, migrate.py, sweep.py, vault_lint.py).
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.
Testing
Everything runs offline against deterministic mocks — no credentials, no live vault:
python3 chorus-memory.plugin.src/skills/chorus-memory/scripts/test_chorus_client.py # unit + routing-sync guard
python3 chorus-memory.plugin.src/skills/chorus-memory/scripts/test_v1_scaffold.py # module interfaces + config
python3 eval/test_features.py # capture/resolve/recall/link/sweep end-to-end
python3 eval/test_multimember.py # 26-check two-member group lifecycle
python3 eval/test_offline_queue.py eval/test_patch_semantics.py eval/test_reflect.py
python3 eval/run_eval.py # retrieval / dedup / write-safety / durability metrics
Version history
| Version | Highlights |
|---|---|
| 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.4 | Phase 4 hardening: bounded lock retry on index writes, attributed duplicate gate, recall --author, load --all-members, N-writer concurrency contract, committed two-member e2e suite. |
| 2.0.0-alpha.3 | Phase 3 (the core): schema 5 per-member namespacing, group profile + group inbox, self-onboarding load, member-tagged shared writes, per-member lint, migrate 4→5. |
| 2.0.0-alpha.2 | Phase 2: {group, member, endpoint, key} config (member required), author: stamped on every agent write, missing-author lint, member-attributed locks/queue, per-member baked builds. |
| 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 repo and at this repo's echo-fork-point tag.