1
0
forked from jason/echo

Remove ECHO back-compat shims — CHORUS is a clean break

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>
This commit is contained in:
2026-07-21 13:42:14 -05:00
parent d366ca4032
commit d920f8821f
16 changed files with 58 additions and 165 deletions
+6 -10
View File
@@ -8,24 +8,20 @@ __pycache__/
.venv/ .venv/
venv/ venv/
# Local CHORUS/ECHO state (never commit — holds the offline queue + cached vault reads) # Local CHORUS state (never commit — holds the offline queue + cached vault reads)
.echo-memory/
.chorus-memory/ .chorus-memory/
# Eval output # Eval output
eval/results/*.json eval/results/*.json
!eval/results/.gitkeep !eval/results/.gitkeep
# CHORUS/ECHO config: the filled-in key file is yours to distribute, never commit it # CHORUS config: the filled-in key file is yours to distribute, never commit it
/echo-memory.config.json
/chorus-memory.config.json /chorus-memory.config.json
/dist/ /dist/
# Build artifacts. Per-user BAKED artifacts (build.py --bake-key) carry a live vault # Build artifacts. Per-user BAKED artifacts (build.py --bake-key) carry a live vault
# bearer token — NEVER commit them; they belong in dist/ (above). As of the CHORUS # bearer token — NEVER commit them; they belong in dist/ (above). Versioned artifacts
# fork (Phase 0), versioned artifacts are no longer tracked in the tree either — # are not tracked in the tree — they ship as Gitea releases. Only the current pointer
# they ship as Gitea releases (per ROADMAP-2.0). Only the current pointer # (chorus-memory.plugin) stays tracked. Historical ECHO artifacts remain at the
# (echo-memory.plugin / chorus-memory.plugin) stays tracked. Historical ECHO # echo-fork-point tag and in the parent jason/echo repo.
# artifacts remain at the echo-fork-point tag and in the parent jason/echo repo.
echo-memory-*.plugin
chorus-memory-*.plugin chorus-memory-*.plugin
+6 -13
View File
@@ -15,23 +15,16 @@ CHORUS is a group-based fork of ECHO (`jason/echo`, v1.5.1, schema 4 — preserv
bootstrap marker to `_agent/chorus-vault.md`; manifest version → 2.0.0-alpha.1. bootstrap marker to `_agent/chorus-vault.md`; manifest version → 2.0.0-alpha.1.
- Docs' endpoint references now `chorusapi.mpm.to`. - Docs' endpoint references now `chorusapi.mpm.to`.
### Back-compat (one major version) ### No ECHO back-compat — clean break
- Legacy `ECHO_*` env vars are adopted as `CHORUS_*` at import when the new name is unset. CHORUS deploys as fresh vaults on fresh machines for new users, so it carries **no**
- A legacy `~/.claude/echo-memory/config.json` is read (never written) when no CHORUS ECHO compatibility: `ECHO_*` env vars, `~/.claude/echo-memory/config.json`,
config exists; `config`/`doctor` report the legacy source. `~/.echo-memory/`, and a vault's `_agent/echo-vault.md` marker are all ignored.
- A legacy `~/.echo-memory/` state dir is honored when `~/.chorus-memory/` doesn't exist (Shims for these were built during Phase 1 and then removed by decision.)
yet, so queued offline writes aren't stranded.
- The bootstrap probe (`load`, `doctor`, `bootstrap`, `vault_lint`, `sweep`, `migrate`)
falls back to a pre-fork `_agent/echo-vault.md` marker; bootstrap's repair pass leaves
a legacy marker in place (the schema migration will rename it) rather than writing a
second marker. `routing.json` gains a read-only `agent-marker-legacy` route.
### Verified ### Verified
- 25/25 client unit tests, scaffold checks, routing-doc sync, and all four mock - 25/25 client unit tests, scaffold checks, routing-doc sync, and all four mock
end-to-end suites green; `run_eval.py` metrics unchanged (0 dupes / 0 false blocks / end-to-end suites green; `run_eval.py` metrics unchanged (0 dupes / 0 false blocks /
0 silent failures / 0 lost offline writes). Six new shim smoke tests (legacy env, 0 silent failures / 0 lost offline writes).
legacy config path, chorus-over-legacy precedence, legacy-marker load/doctor, fresh
vault) pass.
## 1.5.1 ## 1.5.1
+21 -15
View File
@@ -57,7 +57,7 @@ on two machines but not for N people.
its own Obsidian + Local REST API instance exposed there (separate from its own Obsidian + Local REST API instance exposed there (separate from
ECHO's `echoapi.alwisp.com`; the personal ECHO vault is untouched). The ECHO's `echoapi.alwisp.com`; the personal ECHO vault is untouched). The
endpoint stays machine-local/baked config, never in source, same as ECHO. endpoint stays machine-local/baked config, never in source, same as ECHO.
`owner``group` (with `owner` accepted as a legacy alias). Resolution order `owner``group`. Resolution order
unchanged: baked > env (`CHORUS_MEMBER` etc.) > config file. Baked per-user unchanged: baked > env (`CHORUS_MEMBER` etc.) > config file. Baked per-user
builds (`build.py --bake-key --label alice`) already exist and become the builds (`build.py --bake-key --label alice`) already exist and become the
primary **onboarding** mechanism: one artifact per member, member id baked in. primary **onboarding** mechanism: one artifact per member, member id baked in.
@@ -158,10 +158,10 @@ theirs alone). What remains shared-write:
| Commands | `/echo-{load,save,recall,reflect,triage,sweep,health,doctor}` | `/chorus-*` | | Commands | `/echo-{load,save,recall,reflect,triage,sweep,health,doctor}` | `/chorus-*` |
| Scripts | `echo*.py` (16 modules + imports everywhere) | `chorus*.py` | | Scripts | `echo*.py` (16 modules + imports everywhere) | `chorus*.py` |
| Exception | `EchoError` | `ChorusError` | | Exception | `EchoError` | `ChorusError` |
| Env vars | `ECHO_OWNER/BASE/KEY/CONFIG/TODAY/VERIFY/LOCK_TTL/TIMEOUT/WORKERS/STATE_DIR/CLIENT/DUP_GATE/REFLECT_MIN_TURNS/FRESH_HALF_LIFE` | `CHORUS_*` (+ new `CHORUS_MEMBER`); `ECHO_*` read as fallback aliases in `chorus_config` for one major version | | Env vars | `ECHO_OWNER/BASE/KEY/CONFIG/TODAY/VERIFY/LOCK_TTL/TIMEOUT/WORKERS/STATE_DIR/CLIENT/DUP_GATE/REFLECT_MIN_TURNS/FRESH_HALF_LIFE` | `CHORUS_*` (+ new `CHORUS_MEMBER`) |
| Config file | `~/.claude/echo-memory/config.json` | `~/.claude/chorus-memory/config.json` (import path: `config import` accepts an old echo config) | | Config file | `~/.claude/echo-memory/config.json` | `~/.claude/chorus-memory/config.json` |
| State dir | `~/.echo-memory/` | `~/.chorus-memory/` | | State dir | `~/.echo-memory/` | `~/.chorus-memory/` |
| Marker | `_agent/echo-vault.md` (hardcoded in 8 files + routing.json) | `_agent/chorus-vault.md`; probe checks new name **then** old (adoption path) | | Marker | `_agent/echo-vault.md` (hardcoded in 8 files + routing.json) | `_agent/chorus-vault.md` |
| Hooks | `hooks.json` paths ×2 incl. CoWork fallback glob | updated paths | | Hooks | `hooks.json` paths ×2 incl. CoWork fallback glob | updated paths |
| Docs/branding | README, SKILL.md, references, scaffold seeds, icons | rewrite pass (cosmetic but large) | | Docs/branding | README, SKILL.md, references, scaffold seeds, icons | rewrite pass (cosmetic but large) |
@@ -177,11 +177,14 @@ Repo hygiene on the fork: strip stale versioned `.plugin` artifacts from the
tree (move to Gitea releases per ROADMAP-2.0), confirm `eval/` suites green tree (move to Gitea releases per ROADMAP-2.0), confirm `eval/` suites green
as-is, tag the fork point. as-is, tag the fork point.
### Phase 1 — Mechanical rename (12 days) ### Phase 1 — Mechanical rename (12 days) — DONE
Everything in §3, with back-compat shims (env aliases, marker dual-probe, Everything in §3. All existing tests pass under the new names. No behavior
config import from echo paths). All existing tests pass under the new names. change. **Deliverable:** installable `chorus-memory` plugin that is
No behavior change. **Deliverable:** installable `chorus-memory` plugin that is ECHO-with-a-new-name. *(Decided post-Phase-1: CHORUS is a **clean break** —
ECHO-with-a-new-name. fresh vaults on fresh machines for new users, so no ECHO back-compat: legacy
`ECHO_*` env vars, echo-memory configs, `~/.echo-memory` state, and
`_agent/echo-vault.md` markers are all ignored. Shims briefly existed and were
removed.)*
### Phase 2 — Identity plumbing (12 days) ### Phase 2 — Identity plumbing (12 days)
`member` in config (+ `CHORUS_MEMBER`, baked-tier support, `config set --member`), `member` in config (+ `CHORUS_MEMBER`, baked-tier support, `config set --member`),
@@ -198,12 +201,10 @@ if the vault exists but your namespace doesn't — new members self-onboard),
`cmd_load`/`cmd_scope`/heartbeat/inbox/triage/reflect retargeted to member `cmd_load`/`cmd_scope`/heartbeat/inbox/triage/reflect retargeted to member
paths, session logs under `sessions/<member>/`, member-prefixed Agent Log paths, session logs under `sessions/<member>/`, member-prefixed Agent Log
lines, `routing.json` route rows (with `<member>` pattern segment) + retired lines, `routing.json` route rows (with `<member>` pattern segment) + retired
old paths, lint updates, `migrate.py` 4→5 (moves Jason's existing old paths, lint updates, `migrate.py` 4→5 (for any alpha CHORUS vaults stood
operator-preferences/current-context/heartbeat/inbox/sessions into up between phases — there is no ECHO-vault adoption path; CHORUS vaults start
`members/jason/…`, stamps `author: jason` vault-wide via sweep). Docs: fresh). Docs: vault-layout.md, routing-map.md, bootstrap.md rewritten.
vault-layout.md, routing-map.md, bootstrap.md rewritten. **Deliverable:** a real multi-member vault, bootstrapped from empty.
**Deliverable:** a real multi-member vault; the live ECHO vault migrates in
place.
### Phase 4 — Shared-write hardening (12 days) ### Phase 4 — Shared-write hardening (12 days)
Required lock on index writes with bounded retry/backoff, gate messages carry Required lock on index writes with bounded retry/backoff, gate messages carry
@@ -246,3 +247,8 @@ retrieval/durability metrics. New icon.
`_agent/members/<id>/inbox.md`; a shared `inbox/captures/group.md` holds `_agent/members/<id>/inbox.md`; a shared `inbox/captures/group.md` holds
unowned items (member-prefixed lines, idempotent appends), routed by unowned items (member-prefixed lines, idempotent appends), routed by
whoever triages first. Both are probed at load and covered by triage. whoever triages first. Both are probed at load and covered by triage.
6. **Clean break from ECHO** *(added post-Phase-1)*. CHORUS vaults are fresh
vaults on fresh machines for new users — no ECHO adoption path, no legacy
`ECHO_*` env/config/state/marker compatibility. Jason's personal ECHO vault
continues unchanged on its own plugin and endpoint. If ECHO history is ever
wanted in CHORUS, it would be a one-off import exercise, out of scope.
+5 -5
View File
@@ -4,11 +4,11 @@
> Obsidian vault serving a group of members instead of a single operator. The conversion > Obsidian vault serving a group of members instead of a single operator. The conversion
> plan and settled design decisions live in [CHORUS-PLAN.md](CHORUS-PLAN.md). The pre-fork > plan and settled design decisions live in [CHORUS-PLAN.md](CHORUS-PLAN.md). The pre-fork
> ECHO v1.5.1 state is preserved at the `echo-fork-point` tag. **Phase 1 (mechanical > ECHO v1.5.1 state is preserved at the `echo-fork-point` tag. **Phase 1 (mechanical
> rename, back-compat shims) is done**; the body of this README below the fold still > rename) is done**; the body of this README below the fold still describes the
> describes the inherited single-operator behavior and is rewritten in Phase 5. > inherited single-operator behavior and is rewritten in Phase 5.
> Deployment endpoint: `https://chorusapi.mpm.to`. Legacy `ECHO_*` env vars, an existing > Deployment endpoint: `https://chorusapi.mpm.to`. CHORUS is a **clean break** from
> `~/.claude/echo-memory/config.json`, and a vault's old `_agent/echo-vault.md` marker > ECHO — fresh vaults on fresh machines; it carries no ECHO back-compat and never
> all keep working for one major version. > reads ECHO configs, state dirs, or vault markers.
Persistent memory for Claude / CoWork sessions via the **CHORUS** Obsidian vault, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). 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`. Persistent memory for Claude / CoWork sessions via the **CHORUS** Obsidian vault, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). 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`.
Binary file not shown.
@@ -79,7 +79,6 @@ Lifecycle folders; `status:` frontmatter MUST equal the folder name (the linter
| Path | Trigger | What lands | Distinct because | Method | | Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------| |------|---------|------------|------------------|--------|
| `_agent/chorus-vault.md` | Bootstrap / schema migration only | Marker: `schema_version`, bootstrap date | Plugin-owned probe; never hand-edited | GET / PUT | | `_agent/chorus-vault.md` | Bootstrap / schema migration only | Marker: `schema_version`, bootstrap date | Plugin-owned probe; never hand-edited | GET / PUT |
| `_agent/echo-vault.md` | Pre-fork ECHO vault adoption (read-only probe) | Legacy ECHO marker, honored until the rename migration | CHORUS is a fork of ECHO; adopted vaults still carry the old marker | GET |
| `_agent/context/current-context.md` | Active scope changes; task focus shifts | `## Scope`, `## Scope History`, priorities | Single *live* scope pointer, vs episodic which is a *past* record | PATCH / PUT | | `_agent/context/current-context.md` | Active scope changes; task focus shifts | `## Scope`, `## Scope History`, priorities | Single *live* scope pointer, vs episodic which is a *past* record | PATCH / PUT |
| `_agent/memory/semantic/operator-preferences.md` | A preference/pattern about the operator | Append under `## Observations`; promote to `## Fact / Pattern` when stable | The one curated profile; distinct from ad-hoc semantic notes | PATCH | | `_agent/memory/semantic/operator-preferences.md` | A preference/pattern about the operator | Append under `## Observations`; promote to `## Fact / Pattern` when stable | The one curated profile; distinct from ad-hoc semantic notes | PATCH |
| `_agent/memory/semantic/<slug>.md` | A durable fact/pattern that isn't an operator-preference | Semantic note | Timeless fact, vs episodic (time-stamped event) and working (transient) | PUT | | `_agent/memory/semantic/<slug>.md` | A durable fact/pattern that isn't an operator-preference | Semantic note | Timeless fact, vs episodic (time-stamped event) and working (transient) | PUT |
@@ -85,17 +85,14 @@ def main(argv: list[str] | None = None) -> int:
print(f"bootstrap: scaffold not found at {SCAFFOLD}", file=sys.stderr) print(f"bootstrap: scaffold not found at {SCAFFOLD}", file=sys.stderr)
return 1 return 1
legacy_marker = (not exists(chorus.MARKER_PATH)) and exists(chorus.LEGACY_MARKER_PATH) if exists(chorus.MARKER_PATH):
if exists(chorus.MARKER_PATH) or legacy_marker: status, body = chorus.request("GET", chorus.vault_url(chorus.MARKER_PATH))
probe = chorus.LEGACY_MARKER_PATH if legacy_marker else chorus.MARKER_PATH
status, body = chorus.request("GET", chorus.vault_url(probe))
version = "unknown" version = "unknown"
if status == 200: if status == 200:
version = next((ln.split(":", 1)[1].strip() version = next((ln.split(":", 1)[1].strip()
for ln in body.decode(errors="replace").splitlines() for ln in body.decode(errors="replace").splitlines()
if ln.startswith("schema_version:")), "unknown") if ln.startswith("schema_version:")), "unknown")
note = " [legacy ECHO marker]" if legacy_marker else "" print(f"bootstrap: marker present (schema_version={version}). Running repair pass (fills only missing files).")
print(f"bootstrap: marker present{note} (schema_version={version}). Running repair pass (fills only missing files).")
for folder in LEAVES: for folder in LEAVES:
leaf_readme(folder, args.dry_run) leaf_readme(folder, args.dry_run)
@@ -109,13 +106,7 @@ def main(argv: list[str] | None = None) -> int:
seed("_agent/context/current-context.md", SCAFFOLD / "anchors" / "current-context.seed.md", args.dry_run) seed("_agent/context/current-context.md", SCAFFOLD / "anchors" / "current-context.seed.md", args.dry_run)
seed("inbox/captures/inbox.md", SCAFFOLD / "anchors" / "inbox.seed.md", args.dry_run) seed("inbox/captures/inbox.md", SCAFFOLD / "anchors" / "inbox.seed.md", args.dry_run)
seed("README.md", SCAFFOLD / "README.vault.md", args.dry_run) seed("README.md", SCAFFOLD / "README.vault.md", args.dry_run)
if legacy_marker: seed(chorus.MARKER_PATH, SCAFFOLD / "chorus-vault.md", args.dry_run) # marker LAST
# Adopted pre-fork ECHO vault: leave the legacy marker as the single
# authoritative probe — the schema migration renames it. Two markers
# would make "which is authoritative" ambiguous.
print("bootstrap: legacy ECHO marker present — not writing the CHORUS marker (migrate.py renames it).")
else:
seed(chorus.MARKER_PATH, SCAFFOLD / "chorus-vault.md", args.dry_run) # marker LAST
print(f"bootstrap: done ({'DRY-RUN ' if args.dry_run else ''}{chorus.today()}).") print(f"bootstrap: done ({'DRY-RUN ' if args.dry_run else ''}{chorus.today()}).")
print("bootstrap: next: create today's daily note + a bootstrap session log + heartbeat (see SKILL.md).") print("bootstrap: next: create today's daily note + a bootstrap session log + heartbeat (see SKILL.md).")
@@ -101,21 +101,6 @@ MAX_WORKERS = max(1, int(os.environ.get("CHORUS_WORKERS", "8")))
LOCK_PATH = "_agent/locks/vault.lock" LOCK_PATH = "_agent/locks/vault.lock"
MARKER_PATH = "_agent/chorus-vault.md" MARKER_PATH = "_agent/chorus-vault.md"
# Pre-fork ECHO vaults carry the old marker name. It is honored read-only as a
# bootstrap probe (vault-adoption path); the schema migration renames it.
LEGACY_MARKER_PATH = "_agent/echo-vault.md"
def marker_get():
"""GET the bootstrap marker: the CHORUS name first, then the legacy ECHO
name. Returns (path, status, body) — on a double miss, the CHORUS path with
its 404 (or error) result."""
status, body = request("GET", vault_url(MARKER_PATH))
if status == 404:
l_status, l_body = request("GET", vault_url(LEGACY_MARKER_PATH))
if l_status == 200:
return LEGACY_MARKER_PATH, l_status, l_body
return MARKER_PATH, status, body
class ChorusError(RuntimeError): class ChorusError(RuntimeError):
@@ -655,10 +640,7 @@ def cmd_load() -> int:
heartbeat_absent = False heartbeat_absent = False
offline = False offline = False
for label, path in targets: for label, path in targets:
if label == "marker": status, body = request("GET", vault_url(path))
path, status, body = marker_get() # falls back to a legacy ECHO marker
else:
status, body = request("GET", vault_url(path))
if status == 200: if status == 200:
chorus_queue.cache_put(path, body) # refresh last-known-good for offline fallback chorus_queue.cache_put(path, body) # refresh last-known-good for offline fallback
print(f"===== {label}: {path} (HTTP 200) =====") print(f"===== {label}: {path} (HTTP 200) =====")
@@ -54,26 +54,6 @@ DEFAULT_OWNER = ""
DEFAULT_BASE = "" DEFAULT_BASE = ""
DEFAULT_KEY = "" DEFAULT_KEY = ""
# ---------------------------------------------------------------- legacy shim
# CHORUS is a fork of ECHO. For one major version, legacy ECHO_* environment
# variables keep working: at import time each ECHO_<X> is adopted as CHORUS_<X>
# whenever CHORUS_<X> is not itself set. Scripts only ever read CHORUS_*.
_LEGACY_ENV_SUFFIXES = (
"OWNER", "BASE", "KEY", "CONFIG", "TODAY", "VERIFY", "LOCK_TTL", "TIMEOUT",
"WORKERS", "STATE_DIR", "CLIENT", "DUP_GATE", "REFLECT_MIN_TURNS",
"FRESH_HALF_LIFE",
)
def _alias_legacy_env() -> None:
for suffix in _LEGACY_ENV_SUFFIXES:
new, old = f"CHORUS_{suffix}", f"ECHO_{suffix}"
if not os.environ.get(new) and os.environ.get(old):
os.environ[new] = os.environ[old]
_alias_legacy_env()
TEMPLATE = { TEMPLATE = {
"owner": "Full Name — how the agent should refer to the vault owner (third person)", "owner": "Full Name — how the agent should refer to the vault owner (third person)",
"endpoint": "https://your-obsidian-rest-endpoint.example.com", "endpoint": "https://your-obsidian-rest-endpoint.example.com",
@@ -87,32 +67,15 @@ def claude_dir() -> Path:
def config_path() -> Path: def config_path() -> Path:
"""Absolute path to the machine-local config file (the CHORUS path — this is """Absolute path to the machine-local config file."""
where writes always land)."""
override = os.environ.get("CHORUS_CONFIG") override = os.environ.get("CHORUS_CONFIG")
if override: if override:
return Path(override).expanduser() return Path(override).expanduser()
return claude_dir() / "chorus-memory" / "config.json" return claude_dir() / "chorus-memory" / "config.json"
def legacy_config_path() -> Path:
"""The pre-fork ECHO config location, honored read-only as a fallback."""
return claude_dir() / "echo-memory" / "config.json"
def read_config_path() -> Path:
"""The config file to READ: the CHORUS path when overridden or present,
else a legacy ECHO config if one exists (fork adoption path). Writes never
go to the legacy path — `config set`/`import` land at config_path()."""
p = config_path()
if os.environ.get("CHORUS_CONFIG") or p.exists():
return p
legacy = legacy_config_path()
return legacy if legacy.exists() else p
def _from_file() -> dict: def _from_file() -> dict:
p = read_config_path() p = config_path()
if not p.exists(): if not p.exists():
return {} return {}
try: try:
@@ -203,9 +166,8 @@ def source(field: str) -> str:
env = {"owner": "CHORUS_OWNER", "endpoint": "CHORUS_BASE", "key": "CHORUS_KEY"}[field] env = {"owner": "CHORUS_OWNER", "endpoint": "CHORUS_BASE", "key": "CHORUS_KEY"}[field]
if os.environ.get(env): if os.environ.get(env):
return f"{env} env" return f"{env} env"
rp = read_config_path() if config_path().exists() and _from_file().get(field):
if rp.exists() and _from_file().get(field): return "config file"
return "config file" if rp == config_path() else "config file (legacy echo-memory path)"
if baked: if baked:
return "baked-in (plugin)" return "baked-in (plugin)"
return "unset" return "unset"
@@ -59,8 +59,7 @@ def run() -> int:
return 1 return 1
# 3. reachability + auth + marker, in one GET of the bootstrap marker # 3. reachability + auth + marker, in one GET of the bootstrap marker
# (marker_get falls back to a pre-fork ECHO marker on adopted vaults) status, body = chorus.request("GET", chorus.vault_url(chorus.MARKER_PATH))
marker_path, status, body = chorus.marker_get()
if status == 0: if status == 0:
line(False, "vault reachable", body.decode(errors="replace")[:120]) line(False, "vault reachable", body.decode(errors="replace")[:120])
print("\ndoctor: endpoint unreachable — is Obsidian + the Local REST API running?") print("\ndoctor: endpoint unreachable — is Obsidian + the Local REST API running?")
@@ -72,8 +71,7 @@ def run() -> int:
text = body.decode(errors="replace") text = body.decode(errors="replace")
ver = next((ln.split(":", 1)[1].strip() for ln in text.splitlines() ver = next((ln.split(":", 1)[1].strip() for ln in text.splitlines()
if ln.startswith("schema_version:")), "?") if ln.startswith("schema_version:")), "?")
legacy = " (legacy ECHO marker — run migrate.py to adopt)" if marker_path == chorus.LEGACY_MARKER_PATH else "" line(True, "vault bootstrapped", f"schema_version={ver}")
line(True, "vault bootstrapped", f"schema_version={ver}{legacy}")
else: else:
line(status < 400, "marker fetch", f"HTTP {status}") line(status < 400, "marker fetch", f"HTTP {status}")
@@ -38,17 +38,7 @@ REASON = (
def state_marker(session_id: str) -> Path: def state_marker(session_id: str) -> Path:
# Mirrors chorus_queue.state_dir() (this hook runs standalone, so the base = Path(os.environ.get("CHORUS_STATE_DIR") or (Path.home() / ".chorus-memory"))
# ECHO->CHORUS env aliasing in chorus_config may not have run): env first
# (either name), else ~/.chorus-memory, honoring a pre-fork ~/.echo-memory
# when the new dir doesn't exist yet.
override = os.environ.get("CHORUS_STATE_DIR") or os.environ.get("ECHO_STATE_DIR")
if override:
base = Path(override)
else:
new = Path.home() / ".chorus-memory"
legacy = Path.home() / ".echo-memory"
base = legacy if (not new.exists() and legacy.exists()) else new
return base / "hook-state" / f"{session_id}.reflect-nudged" return base / "hook-state" / f"{session_id}.reflect-nudged"
@@ -37,16 +37,7 @@ MUTATING = {"PUT", "POST", "PATCH", "DELETE"}
def state_dir() -> Path: def state_dir() -> Path:
# Env first (chorus_config aliases a legacy ECHO_STATE_DIR at import); the return Path(os.environ.get("CHORUS_STATE_DIR") or (Path.home() / ".chorus-memory"))
# default is ~/.chorus-memory, but an existing pre-fork ~/.echo-memory is
# honored when the new dir doesn't exist yet — so queued offline writes
# from an ECHO install aren't stranded by the rename.
override = os.environ.get("CHORUS_STATE_DIR") or os.environ.get("ECHO_STATE_DIR")
if override:
return Path(override)
new = Path.home() / ".chorus-memory"
legacy = Path.home() / ".echo-memory"
return legacy if (not new.exists() and legacy.exists()) else new
def outbox_path() -> Path: def outbox_path() -> Path:
@@ -79,17 +79,8 @@ def do_or_show(apply: bool, desc: str, func=None) -> None:
print(f"migrate: PLAN {desc}") print(f"migrate: PLAN {desc}")
def find_marker() -> str:
"""The vault's marker path: the CHORUS name, else a legacy pre-fork ECHO
marker (the rename itself is a future migration step)."""
for p in ("_agent/chorus-vault.md", "_agent/echo-vault.md"):
if get_text(p) is not None:
return p
return "_agent/chorus-vault.md"
def marker_schema() -> int: def marker_schema() -> int:
marker = get_text(find_marker()) marker = get_text("_agent/chorus-vault.md")
if marker is None: if marker is None:
print("migrate: marker missing — vault not bootstrapped. Run bootstrap.py, not migrate.py.") print("migrate: marker missing — vault not bootstrapped. Run bootstrap.py, not migrate.py.")
raise SystemExit(3) raise SystemExit(3)
@@ -155,9 +146,8 @@ def main(argv: list[str] | None = None) -> int:
print("migrate: [3->4] _agent/index/ already exists (schema 3); no moves needed.") print("migrate: [3->4] _agent/index/ already exists (schema 3); no moves needed.")
print("migrate: [3->4] then run `sweep.py --apply` to build _agent/index/recall-index.json.") print("migrate: [3->4] then run `sweep.py --apply` to build _agent/index/recall-index.json.")
marker_path = find_marker() do_or_show(args.apply, f"set _agent/chorus-vault.md schema_version -> {CURRENT_SCHEMA}",
do_or_show(args.apply, f"set {marker_path} schema_version -> {CURRENT_SCHEMA}", lambda: chorus.cmd_fm("_agent/chorus-vault.md", "schema_version", str(CURRENT_SCHEMA)))
lambda: chorus.cmd_fm(marker_path, "schema_version", str(CURRENT_SCHEMA)))
if args.apply: if args.apply:
print(f"migrate: migration complete -> schema {CURRENT_SCHEMA}. Run sweep.py --apply, then vault_lint.py.") print(f"migrate: migration complete -> schema {CURRENT_SCHEMA}. Run sweep.py --apply, then vault_lint.py.")
else: else:
@@ -31,7 +31,6 @@
{ "id": "decisions-template", "pattern": "^decisions/decision-template\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Template, not a decision" }, { "id": "decisions-template", "pattern": "^decisions/decision-template\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Template, not a decision" },
{ "id": "agent-marker", "pattern": "^_agent/chorus-vault\\.md$", "method": "PUT", "trigger": "Bootstrap / schema migration only", "distinct_because": "Plugin-owned probe; never hand-edited" }, { "id": "agent-marker", "pattern": "^_agent/chorus-vault\\.md$", "method": "PUT", "trigger": "Bootstrap / schema migration only", "distinct_because": "Plugin-owned probe; never hand-edited" },
{ "id": "agent-marker-legacy", "pattern": "^_agent/echo-vault\\.md$", "method": "GET", "trigger": "Pre-fork ECHO vault adoption (read-only probe)", "distinct_because": "Legacy ECHO marker honored until the rename migration" },
{ "id": "agent-context", "pattern": "^_agent/context/[^/]+\\.md$", "method": "PATCH", "trigger": "Active scope changes / task bundles", "distinct_because": "Single live scope pointer + bundles" }, { "id": "agent-context", "pattern": "^_agent/context/[^/]+\\.md$", "method": "PATCH", "trigger": "Active scope changes / task bundles", "distinct_because": "Single live scope pointer + bundles" },
{ "id": "agent-semantic", "pattern": "^_agent/memory/semantic/[^/]+\\.md$", "method": "PUT", "trigger": "A durable fact/pattern (incl. operator-preferences.md)", "distinct_because": "Timeless fact" }, { "id": "agent-semantic", "pattern": "^_agent/memory/semantic/[^/]+\\.md$", "method": "PUT", "trigger": "A durable fact/pattern (incl. operator-preferences.md)", "distinct_because": "Timeless fact" },
{ "id": "agent-episodic", "pattern": "^_agent/memory/episodic/[^/]+\\.md$", "method": "PUT", "trigger": "A record of what happened, when", "distinct_because": "Anchored to an event in time" }, { "id": "agent-episodic", "pattern": "^_agent/memory/episodic/[^/]+\\.md$", "method": "PUT", "trigger": "A record of what happened, when", "distinct_because": "Anchored to an event in time" },
@@ -109,7 +109,7 @@ def main(argv: list[str] | None = None) -> int:
apply = args.apply apply = args.apply
tag = "APPLY" if apply else "PLAN " tag = "APPLY" if apply else "PLAN "
if get("_agent/chorus-vault.md") is None and get("_agent/echo-vault.md") is None: if get("_agent/chorus-vault.md") is None:
print("sweep: marker missing — vault not bootstrapped. Run bootstrap.py first.", file=sys.stderr) print("sweep: marker missing — vault not bootstrapped. Run bootstrap.py first.", file=sys.stderr)
return 3 return 3
@@ -232,16 +232,12 @@ def main(argv: list[str] | None = None) -> int:
links.add_one(tp, sp) links.add_one(tp, sp)
# ---- 5. stamp schema ------------------------------------------------------ # ---- 5. stamp schema ------------------------------------------------------
marker_path = "_agent/chorus-vault.md" marker = get("_agent/chorus-vault.md") or ""
marker = get(marker_path)
if marker is None: # adopted pre-fork ECHO vault — stamp the marker it has
marker_path = "_agent/echo-vault.md"
marker = get(marker_path) or ""
cur = next((int(m) for m in re.findall(r"(?m)^schema_version:\s*(\d+)", marker)), 0) cur = next((int(m) for m in re.findall(r"(?m)^schema_version:\s*(\d+)", marker)), 0)
if cur < CURRENT_SCHEMA: if cur < CURRENT_SCHEMA:
print(f"sweep: {tag} schema_version {cur} -> {CURRENT_SCHEMA}") print(f"sweep: {tag} schema_version {cur} -> {CURRENT_SCHEMA}")
if apply: if apply:
chorus.cmd_fm(marker_path, "schema_version", str(CURRENT_SCHEMA)) chorus.cmd_fm("_agent/chorus-vault.md", "schema_version", str(CURRENT_SCHEMA))
print(f"\nsweep: {'done — run vault_lint.py to confirm invariants' if apply else 'dry-run complete. Re-run with --apply to write.'}") print(f"\nsweep: {'done — run vault_lint.py to confirm invariants' if apply else 'dry-run complete. Re-run with --apply to write.'}")
return 0 return 0
@@ -145,7 +145,7 @@ def route_matchers():
def main() -> int: def main() -> int:
try: try:
if get("_agent/chorus-vault.md") is None and get("_agent/echo-vault.md") is None: if get("_agent/chorus-vault.md") is None:
print("vault-lint: marker missing — vault not bootstrapped (run bootstrap.py).", file=sys.stderr) print("vault-lint: marker missing — vault not bootstrapped (run bootstrap.py).", file=sys.stderr)
return 3 return 3
except Exception as exc: except Exception as exc: