1
0
forked from jason/echo

Phase 1: mechanical echo→chorus rename with back-compat shims

Identity rename, no behavior change (CHORUS-PLAN.md Phase 1):
- Plugin echo-memory → chorus-memory: manifest (v2.0.0-alpha.1), skill dir,
  16 scripts (chorus.py, chorus_config.py, …), EchoError → ChorusError,
  /chorus-* commands, hook paths, docs, scaffold seeds, eval harness,
  build.py. Docs endpoint → chorusapi.mpm.to.
- Env ECHO_* → CHORUS_*; config → ~/.claude/chorus-memory/config.json;
  state dir → ~/.chorus-memory/; marker → _agent/chorus-vault.md.

Back-compat shims (one major version):
- chorus_config aliases legacy ECHO_* env at import; reads a legacy
  echo-memory config.json when no CHORUS config exists (writes never
  land there); doctor/config report the legacy source.
- State dir honors an existing ~/.echo-memory when the new dir is absent
  (offline queue not stranded).
- Marker dual-probe in load/doctor/bootstrap/lint/sweep/migrate: a
  pre-fork _agent/echo-vault.md counts as bootstrapped; bootstrap repair
  won't write a second marker; routing gains agent-marker-legacy (GET).

Verified: 25/25 unit tests, scaffold + routing-sync checks, 4 mock e2e
suites, run_eval metrics unchanged, +6 shim smoke tests green.
Rebuilt chorus-memory.plugin (79 entries).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 13:35:51 -05:00
parent ff2f2efd0b
commit d366ca4032
98 changed files with 1312 additions and 1164 deletions
+24 -24
View File
@@ -1,20 +1,20 @@
#!/usr/bin/env python3
"""build.py — package the echo-memory plugin source into a .plugin artifact.
"""build.py — package the chorus-memory plugin source into a .plugin artifact.
Zips the CONTENTS of echo-memory.plugin.src/ at the archive root (the layout the plugin
Zips the CONTENTS of chorus-memory.plugin.src/ at the archive root (the layout the plugin
loader expects: .claude-plugin/plugin.json, commands/, skills/ all at top level), excluding
dev cruft. The version is read from the manifest, so the output is named automatically.
Usage:
python build.py # build echo-memory-<version>.plugin + refresh echo-memory.plugin
python build.py # build chorus-memory-<version>.plugin + refresh chorus-memory.plugin
# (token-free: ships no credentials; user configures at runtime)
python build.py --bake-key --from coworker.json [--label alice]
# SECRET-BEARING per-user artifact: bake owner/endpoint/key from a
# config.json (or --owner/--endpoint/--key, or ECHO_* env) into the
# echo_config DEFAULT_* constants. Defaults to dist/ and skips the
# config.json (or --owner/--endpoint/--key, or CHORUS_* env) into the
# chorus_config DEFAULT_* constants. Defaults to dist/ and skips the
# shared pointer. Deliver directly to that one user — NEVER commit it.
python build.py --strip-key # force-blank the DEFAULT_* constants -> guaranteed token-free artifact
python build.py --no-pointer # don't update the echo-memory.plugin "current" pointer
python build.py --no-pointer # don't update the chorus-memory.plugin "current" pointer
python build.py --outdir dist # write artifacts somewhere other than the repo root
Deterministic: entries are sorted and stamped with a fixed timestamp, so an unchanged
@@ -39,10 +39,10 @@ for _stream in (sys.stdout, sys.stderr):
pass
REPO = Path(__file__).resolve().parent
SRC = REPO / "echo-memory.plugin.src"
SRC = REPO / "chorus-memory.plugin.src"
MANIFEST = "/".join([".claude-plugin", "plugin.json"])
# The baked DEFAULT_* constants live in echo_config.py (resolution lowest tier).
CONFIG_PY = "/".join(["skills", "echo-memory", "scripts", "echo_config.py"])
# The baked DEFAULT_* constants live in chorus_config.py (resolution lowest tier).
CONFIG_PY = "/".join(["skills", "chorus-memory", "scripts", "chorus_config.py"])
EXCLUDE_DIRS = {"__pycache__", ".git"}
EXCLUDE_NAMES = {".DS_Store"}
@@ -51,7 +51,7 @@ FIXED_DATE = (2026, 1, 1, 0, 0, 0) # stable timestamp for reproducible archives
MAX_DESCRIPTION = 500 # plugin-marketplace cap: plugin.json "description" must be UNDER this
# (it has silently regressed past the limit before — fail the build now)
# field name -> the constant assigned in echo_config.py
# field name -> the constant assigned in chorus_config.py
_CONSTS = {"owner": "DEFAULT_OWNER", "endpoint": "DEFAULT_BASE", "key": "DEFAULT_KEY"}
@@ -74,7 +74,7 @@ def included_files() -> list[Path]:
def file_bytes(path: Path, arcname: str, bake: dict | None, strip_key: bool) -> bytes:
"""Return the bytes to archive. For echo_config.py, optionally rewrite the
"""Return the bytes to archive. For chorus_config.py, optionally rewrite the
DEFAULT_* constants — inject `bake` values, or blank them with --strip-key."""
data = path.read_bytes()
if arcname != CONFIG_PY or (bake is None and not strip_key):
@@ -90,7 +90,7 @@ def file_bytes(path: Path, arcname: str, bake: dict | None, strip_key: bool) ->
def token_present(path: Path) -> bool:
"""True if any DEFAULT_* constant in echo_config.py holds a non-empty value."""
"""True if any DEFAULT_* constant in chorus_config.py holds a non-empty value."""
text = path.read_text(encoding="utf-8")
for const in _CONSTS.values():
m = re.search(r'^' + re.escape(const) + r'\s*=\s*"(.*)"\s*$', text, re.M)
@@ -114,7 +114,7 @@ def build(out: Path, files: list[Path], bake: dict | None, strip_key: bool) -> i
def bake_values(args) -> dict:
"""Resolve owner/endpoint/key for --bake-key from --owner/--endpoint/--key,
else --from <config.json>, else ECHO_OWNER/ECHO_BASE/ECHO_KEY env."""
else --from <config.json>, else CHORUS_OWNER/CHORUS_BASE/CHORUS_KEY env."""
src = {}
if args.from_file:
fp = Path(args.from_file).expanduser()
@@ -127,23 +127,23 @@ def bake_values(args) -> dict:
if isinstance(loaded, dict):
src = loaded
vals = {
"owner": args.owner or src.get("owner") or os.environ.get("ECHO_OWNER") or "",
"endpoint": (args.endpoint or src.get("endpoint") or os.environ.get("ECHO_BASE") or "").rstrip("/"),
"key": args.key or src.get("key") or os.environ.get("ECHO_KEY") or "",
"owner": args.owner or src.get("owner") or os.environ.get("CHORUS_OWNER") or "",
"endpoint": (args.endpoint or src.get("endpoint") or os.environ.get("CHORUS_BASE") or "").rstrip("/"),
"key": args.key or src.get("key") or os.environ.get("CHORUS_KEY") or "",
}
missing = [f for f in ("endpoint", "key") if not vals[f]]
if missing:
raise RuntimeError(
"build: --bake-key needs a real endpoint and key — missing "
+ ", ".join(missing) + ". Provide --from <config.json>, the --owner/"
"--endpoint/--key flags, or set ECHO_OWNER/ECHO_BASE/ECHO_KEY.")
"--endpoint/--key flags, or set CHORUS_OWNER/CHORUS_BASE/CHORUS_KEY.")
if "your-obsidian-rest-endpoint" in vals["endpoint"] or vals["key"].startswith("<"):
raise RuntimeError("build: --bake-key got the template placeholders, not real values.")
return vals
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Build the echo-memory .plugin artifact")
ap = argparse.ArgumentParser(description="Build the chorus-memory .plugin artifact")
ap.add_argument("--bake-key", action="store_true",
help="bake a user's owner/endpoint/key into the artifact (secret-bearing — never commit)")
ap.add_argument("--from", dest="from_file", metavar="FILE",
@@ -155,7 +155,7 @@ def main(argv: list[str] | None = None) -> int:
ap.add_argument("--strip-key", action="store_true",
help="force-blank the DEFAULT_* constants so no token ships in the artifact")
ap.add_argument("--no-pointer", action="store_true",
help="do not refresh the echo-memory.plugin 'current' pointer")
help="do not refresh the chorus-memory.plugin 'current' pointer")
ap.add_argument("--outdir", help="output directory (default: repo root; dist/ when --bake-key)")
args = ap.parse_args(argv)
@@ -191,15 +191,15 @@ def main(argv: list[str] | None = None) -> int:
return 1
# Baked artifacts are secret-bearing: default them into dist/ (gitignored) and
# never touch the shared, committable echo-memory.plugin pointer.
# never touch the shared, committable chorus-memory.plugin pointer.
default_outdir = (REPO / "dist") if bake else REPO
outdir = Path(args.outdir) if args.outdir else default_outdir
outdir.mkdir(parents=True, exist_ok=True)
label = f"-{args.label}" if args.label else ""
targets = [outdir / f"echo-memory-{version}{label}.plugin"]
targets = [outdir / f"chorus-memory-{version}{label}.plugin"]
if not args.no_pointer and not bake:
targets.append(outdir / "echo-memory.plugin")
targets.append(outdir / "chorus-memory.plugin")
for out in targets:
size = build(out, files, bake, args.strip_key)
@@ -212,8 +212,8 @@ def main(argv: list[str] | None = None) -> int:
"user and NEVER commit, push, or publish it (dist/ is gitignored).")
elif args.strip_key:
print("note: --strip-key set — artifact is token-free; user configures owner/endpoint/key at runtime.")
elif token_present(SRC / "skills" / "echo-memory" / "scripts" / "echo_config.py"):
print("WARNING: a DEFAULT_* constant in echo_config.py is non-empty — this artifact carries a "
elif token_present(SRC / "skills" / "chorus-memory" / "scripts" / "chorus_config.py"):
print("WARNING: a DEFAULT_* constant in chorus_config.py is non-empty — this artifact carries a "
"secret. Do not commit it. Build from a clean source tree, or use --strip-key.")
return 0