Decision: CHORUS deploys as fresh vaults on new machines for different users, so no ECHO adoption path is needed. Removed the Phase-1 shims: - chorus_config: legacy ECHO_* env aliasing and the read-only ~/.claude/echo-memory/config.json fallback - chorus_queue / chorus_hook_stop: ~/.echo-memory state-dir fallback - load/doctor/bootstrap/vault_lint/sweep/migrate: the _agent/echo-vault.md marker dual-probe; routing drops agent-marker-legacy (36 routes) - .gitignore, README, CHANGELOG, CHORUS-PLAN.md updated (decision #6; Phase 3 no longer migrates the live ECHO vault — fresh bootstrap only) All suites green: 25/25 unit, scaffold, routing-sync, 4 mock e2e, run_eval metrics unchanged. Rebuilt chorus-memory.plugin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
Changelog
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.