The final phase of CHORUS-PLAN.md. Code- and doc-complete for group use; rc pends live deployment at chorusapi.mpm.to. - Repo README rewritten for CHORUS: group model (schema 5), concurrency posture, configuration, baked-build onboarding, layout, testing, 2.0 version history (ECHO history referenced at the fork tag). - Operator→member voice sweep across SKILL.md (incl. the trigger description, now group-aware), all references, 8 command docs, plugin README; member preferences / semantic layer / triage guidance describe the group model. - docs/ONBOARDING.md: member runbook — vault stand-up, per-member config, bake, deliver, self-onboarding first load, norms, offboarding. - docs/USAGE.md rewritten as the member usage guide. - New icon: chorus-icon.svg + 1024/512/64 PNGs (three voices, one center). - Removed ECHO-era assets (icons, spec PDF, perf briefs); ECHO planning docs retired to docs/history/. All preserved at echo-fork-point. - eval/README documents test_multimember.py. Manifest -> 2.0.0-rc.1; rebuilt chorus-memory.plugin. Verified: routing-sync, 25/25 unit, scaffold, 5 mock e2e suites, run_eval metrics green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
18 KiB
Changelog
2.0.0-rc.1 — Phase 5 (docs, onboarding, icon) — plan complete
The last phase of the CHORUS conversion plan. The plugin is code- and doc-complete
for group use; rc status pends only live deployment at chorusapi.mpm.to.
Changed
- Group-voice docs everywhere. Repo README fully rewritten for CHORUS (group
model, concurrency posture, configuration, baked-build onboarding, schema-5
layout, testing, 2.0 version history — ECHO history now referenced at the fork
tag). SKILL.md description + remaining prose, all references, commands, and the
plugin README swept operator→member; each member's
preferences.mdsection, the semantic layer, and the triage routing guidance now describe the group model. docs/USAGE.mdrewritten as the member usage guide (automatic behaviors, commands incl.--author/--all-members, group etiquette, troubleshooting).
Added
docs/ONBOARDING.md— the member-onboarding runbook: vault stand-up, per-member config,build.py --bake-key, delivery, self-onboarding first load, first-session norms, offboarding = key rotation, troubleshooting table.- CHORUS icon —
chorus-icon.svg(+ 1024/512/64 PNGs): three voices (teal / violet / amber) radiating waves around one shared center.
Removed
- ECHO-era assets: echo icons, the ECHO spec PDF and performance briefs, and the
ECHO planning docs (
TODO-1.6.md,ROADMAP-2.0.md→docs/history/). All remain at theecho-fork-pointtag and injason/echo.
2.0.0-alpha.4 — Phase 4 (shared-write hardening + group lenses)
Added
- Bounded lock retry on index writes.
vault_lockgainsretries/backoff(linear);atomic_index_updateand the recall-index upkeep now retry acquisition (CHORUS_LOCK_RETRIES, default 4 /CHORUS_LOCK_BACKOFF, default 0.5s). Index lock holds are sub-second, so a retrying writer virtually always acquires; only after exhausted retries does it proceed unlocked — loudly — because an already-written note must never be stranded unindexed. - Duplicate-gate attribution. Gate candidates now carry the existing note's
author; the STOP message readscandidate: acme-rockets -> … (score 1.0, created by jason), so a blocked member knows whose entity they collided with. recall --author <member>— filter hits and linked context to one member's notes (a lens, not a wall); recall briefs and--jsonsummaries now surface theauthor:stamp alongside updated/status.load --all-members— appends a compact group-wide orientation: every member's active scope + last-session heartbeat ("what has the team been doing").references/operating-contract.mdconcurrency section rewritten for N members (namespacing as first defense; member-tagged idempotent appends; retried index lock; member-attributed lock owners).- Eval: the two-member end-to-end suite is now committed as
eval/test_multimember.py(26 checks, incl. the four Phase-4 behaviors).
Fixed
load --all-membersroster parsing accepts folders as trailing-slash entries infiles(the shape the REST API actually returns) as well as afolderskey.
2.0.0-alpha.3 — Phase 3 (per-member namespacing — schema 5)
The core of the group conversion: one shared vault, per-member namespaces for all churn-prone state. A CHORUS vault now bootstraps multi-member from empty.
Added — vault layout (schema 5)
_agent/members/<member>/— each member'spreferences.md,current-context.md(scope + history),heartbeat.md(last-session pointer, written at session end), andinbox.md. Members never contend on each other's churn-prone files._agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md— member-namespaced session logs (no same-minute collisions, filterable per member)._agent/memory/semantic/group-profile.md— shared group facts, conventions, and a## Membersroster (members are rostered automatically at onboarding).inbox/captures/group.md— shared inbox for unowned member-tagged items; whoever triages first routes them.
Added — behavior
- Self-onboarding:
chorus.py loaddetects a bootstrapped vault that lacks THIS member's namespace and stands it up automatically (anchors + sessions dir + roster line). New members install their baked plugin and just start. loadis now 8 orientation reads: marker → group profile → my preferences / context / heartbeat → today's shared daily note → my inbox → group inbox; the heartbeat fallback lists MY sessions dir.scope show/setoperates on the member's own current-context; drift counts only that member's sessions.- Member-tagged shared writes: daily
## Agent Loglines and entity-update dated blocks are written as- YYYY-MM-DD [member]: …;capture --inboxlands in the member's own inbox; triage lists BOTH inboxes (tagged mine/group) and writes member-attributed audit lines. bootstrap.pyseeds group anchors + the configured member's namespace (member_bootstrap()is reusable); the marker is schema 5;migrate.pygains a 4→5 step for alpha vaults (moves pre-5 anchors into the configured member's namespace; no ECHO adoption path).routing.json: newmember-anchors,inbox-group, member-segmented sessions routes; the old single-user anchor paths (shared inbox, operator-preferences, current-context, heartbeat, flat sessions) are retired.vault_lintruns aging-inbox and scope-drift checks per member across all members found in the vault. Docs (SKILL, vault-layout, routing-map, bootstrap) updated to match.
Verified
- All suites green (25/25 unit, scaffold, routing-sync ×36 routes, 4 mock e2e, run_eval metrics unchanged) plus a 19-check two-member end-to-end smoke: jason bootstraps → alice self-onboards via load → interleaved member-tagged daily log → cross-member entity update attributed → independent scopes → triage separates personal vs group items → lint clean.
2.0.0-alpha.2 — Phase 2 (identity plumbing)
Every write is now attributed to a member. Vault layout unchanged (that's Phase 3).
Added
- Config schema is now
{group, member, endpoint, key}(was{owner, endpoint, key}).memberis REQUIRED — a kebab-case slug (^[a-z0-9][a-z0-9-]{0,31}$) identifying who this machine writes as;groupis the descriptive display name for the team sharing the vault. Env:CHORUS_GROUP/CHORUS_MEMBER.config setgains--group/--member(with slug validation);config/doctorreport both with sources; an unconfigured member fails loud (NOT CONFIGURED, exit 78). author:frontmatter on every agent write.capture(_build_note) stamps the configured member; bootstrap stamps seeded anchors via a new{{MEMBER}}substitution; all 8 scaffold templates gain anauthor:line; the canonical frontmatter block (vault-layout.md, SKILL.md) documents it.missing-authorlint check —/chorus-healthflags anyagent_written: truenote without anauthor:(the plugin-owned bootstrap marker is exempt).- Member-attributed concurrency: the advisory-lock owner id is now
<member>-<client>-<pid>(auto_owner), and offline-queue outbox records carry amemberfield for audit. - Per-member baked builds:
build.py --bake-keynow bakes group/member/endpoint/ key (--group/--memberflags; member required + slug-validated), so each delivered artifact knows who it writes as.
Verified
- All suites green (25/25 unit incl. new config/member-validation checks, scaffold, routing-sync, 4 mock e2e incl. a capture-stamps-author assertion, run_eval metrics unchanged) plus 8 end-to-end smoke checks (bootstrap stamping, lint flagging, doctor reporting, NOT-CONFIGURED without member, slug rejection).
2.0.0-alpha.1 — CHORUS fork, Phase 1 (mechanical rename)
CHORUS is a group-based fork of ECHO (jason/echo, v1.5.1, schema 4 — preserved at the
echo-fork-point tag). Phase 1 is the identity rename with no behavior change; see
CHORUS-PLAN.md for the full conversion plan.
Changed
- Plugin renamed
echo-memory→chorus-memory; skill dir, all 16echo*.pymodules (chorus.py,chorus_config.py, …),EchoError→ChorusError, the eight slash commands (/echo-*→/chorus-*), hook script paths, and all docs/scaffold seeds. - Env vars renamed
ECHO_*→CHORUS_*; config moves to~/.claude/chorus-memory/config.json; local state dir to~/.chorus-memory/; bootstrap marker to_agent/chorus-vault.md; manifest version → 2.0.0-alpha.1. - Docs' endpoint references now
chorusapi.mpm.to.
No ECHO back-compat — clean break
CHORUS deploys as fresh vaults on fresh machines for new users, so it carries no
ECHO compatibility: ECHO_* env vars, ~/.claude/echo-memory/config.json,
~/.echo-memory/, and a vault's _agent/echo-vault.md marker are all ignored.
(Shims for these were built during Phase 1 and then removed by decision.)
Verified
- 25/25 client unit tests, scaffold checks, routing-doc sync, and all four mock
end-to-end suites green;
run_eval.pymetrics unchanged (0 dupes / 0 false blocks / 0 silent failures / 0 lost offline writes).
1.5.1
Fixed — duplicate gate over-firing on vault-common tokens and cross-kind names
Live 1.5.0 use surfaced a false positive: creating the decision "echo-memory 1.5.0
improvement set" was blocked by the archived project echo-v-05 (score 1.0), because
score = overlap / min(|tokens|) lets any entity whose single distinctive token
appears in the new title score 1.0 — and "echo" appears across many entities in an
echo-centric vault.
New echo_index.gate_candidates() (backed by token_df()) applies two precision
rules before blocking; capture gates through it. fuzzy_candidates() — the advisory
warning list — is unchanged:
- Same-kind only. A cross-kind name collision (a decision titled after its project, a meeting named for a company) is intentional naming, not a duplicate. Cross-kind lookalikes still warn, never block.
- No single-common-token blocks. A lone shared token gates only when it is unique to that entity (vault df == 1). Multi-token overlaps still gate even when the individual tokens are common.
Exit-76 / --merge-into / --force semantics unchanged. Tests: 3 new offline unit
tests (gate_candidates), end-to-end gate cases restructured + 2 new (cross-kind
no-gate, common-token no-gate); verified against the live vault (the original false
positive now creates cleanly; a same-kind multi-token lookalike still gates).
1.5.0
Six improvements from the July 2026 review + the frontmatter-drift field report.
Fixed — capture-on-update kept only the first body line (silent data loss)
capture on an existing entity appended - <date>: <first line> and discarded the
rest of the body. It now appends the whole body as a dated block — first line on the
bullet, continuation lines indented under it. Idempotency (bullet-per-day) unchanged.
Added — frontmatter completeness (prevent → tool → detect → remediate)
A field audit found 18/71 entity notes missing status/tags; the drift was created
at write time and invisible to the tooling. Four coordinated changes, driven by one
data source (KIND_STATUS / KIND_REQUIRED_FM in echo_index.py):
capturestamps a kind-appropriate defaultstatus(all kinds, not just projects) and seedstagswith the note's kind; new--tags a,benriches it. Reflect/triage proposals accept a"tags"field.echo.py fmis now create-or-replace: on400 invalid-target(missing key) it surgically inserts the one line into the frontmatter block and re-PUTs, verified — it previously failed on exactly the notes that needed repair.vault_lint.pygains a kind-scopedincomplete-frontmattercheck (missing/emptystatus/tagson entity notes; block-style lists handled).sweep.py --applybackfills flagged notes (kind-default status + baseline tag).
Added — pre-write duplicate gate in capture
A strong fuzzy candidate (score ≥ 0.6, env ECHO_DUP_GATE) now stops capture
before it creates a near-duplicate (exit 76 + candidate list) instead of warning
after the file exists. --merge-into <slug> routes the capture as an update to the
existing entity (mention learned as an alias); --force creates anyway. Weak
resemblances still create + warn as before.
Changed — recall corpus + ranking (recall-index schema 2)
- The BM25 corpus now includes session logs and journal notes (down-weighted 0.5 /
0.4 so entity notes win ties) — past discussions are findable without having been
promoted into an entity note.
echo.py putkeeps the index current for corpus paths. - Ranking fuses BM25 with freshness (half-life decay on
updated:, floored at 0.6, envECHO_FRESH_HALF_LIFE) and status (active×1.1,on-hold×0.9,archived×0.75) — a stale archived note no longer outranks the live one. - Hits print
updated:/status:;recall --jsonemits structured results. - The recall index carries per-doc meta (schema 2); an old index is discarded and rebuilt automatically on the next recall/sweep.
Added — session hooks (self-firing load & reflect)
New hooks/hooks.json + two hook scripts. SessionStart runs echo.py load and
injects the output as context (skips resume/compact; NOT-CONFIGURED degrades to an
instruction, never an error). Stop nudges once per substantive session (≥5 user
turns, env ECHO_REFLECT_MIN_TURNS) when nothing was reflected or session-logged —
reflection itself still previews and asks before writing. Both use the 1.4.2 CoWork
path fallback and always exit 0.
Added — one-tap inbox triage + --json on read verbs
New echo.py triage: --list --json parses the inbox into {line, date, text, age_days}; a proposals file (reflect schema + "line") is classified → previewed →
applied via capture, with the inbox/processing-log/ audit line written per move
(originals never deleted). /echo-triage rewritten around it. Also: --json for
recall, link, and scope show (resolve/capture already had it).
Tests: +22 feature cases (mock end-to-end incl. hooks), patch-semantics updated for
the new fm contract; all suites green. Docs: SKILL.md, README, command docs updated.
1.4.2
Fixed — CoWork sandbox plugin-path resolution
In a remote CoWork sandbox, ${CLAUDE_PLUGIN_ROOT} can resolve to a host path the
sandbox can't reach (e.g. /var/folders/…), while the plugin is actually mounted under
…/mnt/.remote-plugins/…. Every python3 "${CLAUDE_PLUGIN_ROOT}/…" invocation then
failed until the agent manually located the real path. (Root cause is the harness env
var, not the plugin — the scripts always used the variable correctly, never a hardcoded
path — but the plugin now self-heals.)
- SKILL.md and all eight slash commands resolve the scripts path with a fallback:
prefer
${CLAUDE_PLUGIN_ROOT}, else locate the copy under/sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/. Reused via$ECHO/$LINT/$SWEEP/$SDIR; on a normal host the primary path always wins, so behaviour is unchanged. echo-health/echo-sweepallowed-toolsbroadened to match the resolved (variable-dir) invocation and thels/dirnameresolver steps.
1.4.1
Fixed — baked credentials now win unconditionally
A per-user baked artifact could still be prompted for credentials on install. The
baked DEFAULT_* tier was lowest priority, so a stale ECHO_* env var or a
leftover/placeholder ~/.claude/echo-memory/config.json shadowed the baked key —
even an unedited "<placeholder>" value won over it, flipping is_configured() to
false and triggering the first-run "ask the operator" flow despite valid baked creds.
echo_config.load()now treats a complete baked set (endpoint + key both present) as authoritative: it wins over env vars and the config file and cannot be shadowed. The generic (unbaked) plugin keeps its env → file → first-run flow.- Added
baked_complete();source()reportsbaked-in (plugin)for that tier.
Security — untracked leaked baked artifacts
- Two labeled baked artifacts (
echo-memory-1.4.0-jason.plugin,echo-memory-1.4.0-gretchen.plugin), each carrying a live vault bearer token, had been committed at the repo root. They are nowgit rm --cached-removed and.gitignoregainsecho-memory-*-*.pluginso labeled builds can't be re-committed (the version-only pointerecho-memory.pluginandecho-memory-<ver>.pluginstay tracked). Tokens remain in prior history — rotate if the repo was ever shared.
1.4.0
Added — per-user baked-key builds (CoWork zero-touch)
The plugin is the only thing reliably mounted into a CoWork session (the sandbox's
~/.claude is synthetic, not your real one), so the config now can travel inside
the artifact. echo_config gains a lowest-priority fallback tier — the
DEFAULT_OWNER / DEFAULT_BASE / DEFAULT_KEY constants — empty in source, so
the committed tree still ships zero credentials.
build.py --bake-key --from <config.json> [--label <name>]substitutes a single user's owner/endpoint/key into those constants, producing a per-user artifact that needs no config file and no per-session paste. Values may also come from--owner/--endpoint/--keyorECHO_OWNER/ECHO_BASE/ECHO_KEY.- Baked builds default to
dist/(gitignored) and never touch the sharedecho-memory.pluginpointer. build.py --strip-keynow force-blanks the same constants (guaranteed token-free).config show/doctorreport[baked-in default]when a field resolves from the baked tier.
Changed — artifact hygiene
.gitignoreexcludesdist/and the filled-inecho-memory.config.json, where baked builds and the key file are meant to live. A baked artifact carries a vault key and must be delivered directly to that one user, never committed, pushed, or published. (Note: labeled baked artifacts built at the repo root were not yet guarded — see 1.4.1.)
Resolution order (unchanged for hosts)
env → $ECHO_CONFIG file → canonical ~/.claude/echo-memory/config.json →
baked DEFAULT_*. On a normal desktop the empty defaults mean behaviour is
identical to 1.3.x; the baked tier only matters in per-user artifacts.