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/
# Local CHORUS/ECHO state (never commit — holds the offline queue + cached vault reads)
.echo-memory/
# Local CHORUS state (never commit — holds the offline queue + cached vault reads)
.chorus-memory/
# Eval output
eval/results/*.json
!eval/results/.gitkeep
# CHORUS/ECHO config: the filled-in key file is yours to distribute, never commit it
/echo-memory.config.json
# CHORUS config: the filled-in key file is yours to distribute, never commit it
/chorus-memory.config.json
/dist/
# 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
# fork (Phase 0), versioned artifacts are no longer tracked in the tree either —
# they ship as Gitea releases (per ROADMAP-2.0). Only the current pointer
# (echo-memory.plugin / chorus-memory.plugin) stays tracked. Historical ECHO
# artifacts remain at the echo-fork-point tag and in the parent jason/echo repo.
echo-memory-*.plugin
# bearer token — NEVER commit them; they belong in dist/ (above). Versioned artifacts
# are not tracked in the tree — they ship as Gitea releases. Only the current pointer
# (chorus-memory.plugin) stays tracked. Historical ECHO artifacts remain at the
# echo-fork-point tag and in the parent jason/echo repo.
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.
- Docs' endpoint references now `chorusapi.mpm.to`.
### Back-compat (one major version)
- Legacy `ECHO_*` env vars are adopted as `CHORUS_*` at import when the new name is unset.
- A legacy `~/.claude/echo-memory/config.json` is read (never written) when no CHORUS
config exists; `config`/`doctor` report the legacy source.
- A legacy `~/.echo-memory/` state dir is honored when `~/.chorus-memory/` doesn't exist
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.
### 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.py` metrics unchanged (0 dupes / 0 false blocks /
0 silent failures / 0 lost offline writes). Six new shim smoke tests (legacy env,
legacy config path, chorus-over-legacy precedence, legacy-marker load/doctor, fresh
vault) pass.
0 silent failures / 0 lost offline writes).
## 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
ECHO's `echoapi.alwisp.com`; the personal ECHO vault is untouched). The
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
builds (`build.py --bake-key --label alice`) already exist and become the
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-*` |
| Scripts | `echo*.py` (16 modules + imports everywhere) | `chorus*.py` |
| 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 |
| Config file | `~/.claude/echo-memory/config.json` | `~/.claude/chorus-memory/config.json` (import path: `config import` accepts an old echo config) |
| 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` |
| 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 |
| 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
as-is, tag the fork point.
### Phase 1 — Mechanical rename (12 days)
Everything in §3, with back-compat shims (env aliases, marker dual-probe,
config import from echo paths). All existing tests pass under the new names.
No behavior change. **Deliverable:** installable `chorus-memory` plugin that is
ECHO-with-a-new-name.
### Phase 1 — Mechanical rename (12 days) — DONE
Everything in §3. All existing tests pass under the new names. No behavior
change. **Deliverable:** installable `chorus-memory` plugin that is
ECHO-with-a-new-name. *(Decided post-Phase-1: CHORUS is a **clean break** —
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)
`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
paths, session logs under `sessions/<member>/`, member-prefixed Agent Log
lines, `routing.json` route rows (with `<member>` pattern segment) + retired
old paths, lint updates, `migrate.py` 4→5 (moves Jason's existing
operator-preferences/current-context/heartbeat/inbox/sessions into
`members/jason/…`, stamps `author: jason` vault-wide via sweep). Docs:
vault-layout.md, routing-map.md, bootstrap.md rewritten.
**Deliverable:** a real multi-member vault; the live ECHO vault migrates in
place.
old paths, lint updates, `migrate.py` 4→5 (for any alpha CHORUS vaults stood
up between phases — there is no ECHO-vault adoption path; CHORUS vaults start
fresh). Docs: vault-layout.md, routing-map.md, bootstrap.md rewritten.
**Deliverable:** a real multi-member vault, bootstrapped from empty.
### Phase 4 — Shared-write hardening (12 days)
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
unowned items (member-prefixed lines, idempotent appends), routed by
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
> 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
> rename, back-compat shims) is done**; the body of this README below the fold still
> describes the inherited single-operator behavior and is rewritten in Phase 5.
> Deployment endpoint: `https://chorusapi.mpm.to`. Legacy `ECHO_*` env vars, an existing
> `~/.claude/echo-memory/config.json`, and a vault's old `_agent/echo-vault.md` marker
> all keep working for one major version.
> rename) is done**; the body of this README below the fold still describes the
> inherited single-operator behavior and is rewritten in Phase 5.
> Deployment endpoint: `https://chorusapi.mpm.to`. CHORUS is a **clean break** from
> ECHO — fresh vaults on fresh machines; it carries no ECHO back-compat and never
> 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`.
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 |
|------|---------|------------|------------------|--------|
| `_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/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 |
@@ -85,17 +85,14 @@ def main(argv: list[str] | None = None) -> int:
print(f"bootstrap: scaffold not found at {SCAFFOLD}", file=sys.stderr)
return 1
legacy_marker = (not exists(chorus.MARKER_PATH)) and exists(chorus.LEGACY_MARKER_PATH)
if exists(chorus.MARKER_PATH) or legacy_marker:
probe = chorus.LEGACY_MARKER_PATH if legacy_marker else chorus.MARKER_PATH
status, body = chorus.request("GET", chorus.vault_url(probe))
if exists(chorus.MARKER_PATH):
status, body = chorus.request("GET", chorus.vault_url(chorus.MARKER_PATH))
version = "unknown"
if status == 200:
version = next((ln.split(":", 1)[1].strip()
for ln in body.decode(errors="replace").splitlines()
if ln.startswith("schema_version:")), "unknown")
note = " [legacy ECHO marker]" if legacy_marker else ""
print(f"bootstrap: marker present{note} (schema_version={version}). Running repair pass (fills only missing files).")
print(f"bootstrap: marker present (schema_version={version}). Running repair pass (fills only missing files).")
for folder in LEAVES:
leaf_readme(folder, args.dry_run)
@@ -109,12 +106,6 @@ def main(argv: list[str] | None = None) -> int:
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("README.md", SCAFFOLD / "README.vault.md", args.dry_run)
if legacy_marker:
# 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()}).")
@@ -101,21 +101,6 @@ MAX_WORKERS = max(1, int(os.environ.get("CHORUS_WORKERS", "8")))
LOCK_PATH = "_agent/locks/vault.lock"
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):
@@ -655,9 +640,6 @@ def cmd_load() -> int:
heartbeat_absent = False
offline = False
for label, path in targets:
if label == "marker":
path, status, body = marker_get() # falls back to a legacy ECHO marker
else:
status, body = request("GET", vault_url(path))
if status == 200:
chorus_queue.cache_put(path, body) # refresh last-known-good for offline fallback
@@ -54,26 +54,6 @@ DEFAULT_OWNER = ""
DEFAULT_BASE = ""
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 = {
"owner": "Full Name — how the agent should refer to the vault owner (third person)",
"endpoint": "https://your-obsidian-rest-endpoint.example.com",
@@ -87,32 +67,15 @@ def claude_dir() -> Path:
def config_path() -> Path:
"""Absolute path to the machine-local config file (the CHORUS path — this is
where writes always land)."""
"""Absolute path to the machine-local config file."""
override = os.environ.get("CHORUS_CONFIG")
if override:
return Path(override).expanduser()
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:
p = read_config_path()
p = config_path()
if not p.exists():
return {}
try:
@@ -203,9 +166,8 @@ def source(field: str) -> str:
env = {"owner": "CHORUS_OWNER", "endpoint": "CHORUS_BASE", "key": "CHORUS_KEY"}[field]
if os.environ.get(env):
return f"{env} env"
rp = read_config_path()
if rp.exists() and _from_file().get(field):
return "config file" if rp == config_path() else "config file (legacy echo-memory path)"
if config_path().exists() and _from_file().get(field):
return "config file"
if baked:
return "baked-in (plugin)"
return "unset"
@@ -59,8 +59,7 @@ def run() -> int:
return 1
# 3. reachability + auth + marker, in one GET of the bootstrap marker
# (marker_get falls back to a pre-fork ECHO marker on adopted vaults)
marker_path, status, body = chorus.marker_get()
status, body = chorus.request("GET", chorus.vault_url(chorus.MARKER_PATH))
if status == 0:
line(False, "vault reachable", body.decode(errors="replace")[:120])
print("\ndoctor: endpoint unreachable — is Obsidian + the Local REST API running?")
@@ -72,8 +71,7 @@ def run() -> int:
text = body.decode(errors="replace")
ver = next((ln.split(":", 1)[1].strip() for ln in text.splitlines()
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}{legacy}")
line(True, "vault bootstrapped", f"schema_version={ver}")
else:
line(status < 400, "marker fetch", f"HTTP {status}")
@@ -38,17 +38,7 @@ REASON = (
def state_marker(session_id: str) -> Path:
# Mirrors chorus_queue.state_dir() (this hook runs standalone, so the
# 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
base = Path(os.environ.get("CHORUS_STATE_DIR") or (Path.home() / ".chorus-memory"))
return base / "hook-state" / f"{session_id}.reflect-nudged"
@@ -37,16 +37,7 @@ MUTATING = {"PUT", "POST", "PATCH", "DELETE"}
def state_dir() -> Path:
# Env first (chorus_config aliases a legacy ECHO_STATE_DIR at import); the
# 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
return Path(os.environ.get("CHORUS_STATE_DIR") or (Path.home() / ".chorus-memory"))
def outbox_path() -> Path:
@@ -79,17 +79,8 @@ def do_or_show(apply: bool, desc: str, func=None) -> None:
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:
marker = get_text(find_marker())
marker = get_text("_agent/chorus-vault.md")
if marker is None:
print("migrate: marker missing — vault not bootstrapped. Run bootstrap.py, not migrate.py.")
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] then run `sweep.py --apply` to build _agent/index/recall-index.json.")
marker_path = find_marker()
do_or_show(args.apply, f"set {marker_path} schema_version -> {CURRENT_SCHEMA}",
lambda: chorus.cmd_fm(marker_path, "schema_version", str(CURRENT_SCHEMA)))
do_or_show(args.apply, f"set _agent/chorus-vault.md schema_version -> {CURRENT_SCHEMA}",
lambda: chorus.cmd_fm("_agent/chorus-vault.md", "schema_version", str(CURRENT_SCHEMA)))
if args.apply:
print(f"migrate: migration complete -> schema {CURRENT_SCHEMA}. Run sweep.py --apply, then vault_lint.py.")
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": "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-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" },
@@ -109,7 +109,7 @@ def main(argv: list[str] | None = None) -> int:
apply = args.apply
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)
return 3
@@ -232,16 +232,12 @@ def main(argv: list[str] | None = None) -> int:
links.add_one(tp, sp)
# ---- 5. stamp schema ------------------------------------------------------
marker_path = "_agent/chorus-vault.md"
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 ""
marker = get("_agent/chorus-vault.md") or ""
cur = next((int(m) for m in re.findall(r"(?m)^schema_version:\s*(\d+)", marker)), 0)
if cur < CURRENT_SCHEMA:
print(f"sweep: {tag} schema_version {cur} -> {CURRENT_SCHEMA}")
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.'}")
return 0
@@ -145,7 +145,7 @@ def route_matchers():
def main() -> int:
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)
return 3
except Exception as exc: