--- name: chorus-memory description: Use the CHORUS Obsidian vault as the operator's persistent memory across Claude/CoWork sessions. Use whenever the operator asks to remember, save, note, log, or capture anything durable — facts, preferences, decisions, schedule changes, commitments — or asks what Claude knows about them, what was discussed or decided before, to check their notes, load their memory/profile/context, add to their inbox, or to track a new project. Trigger even without memory phrasing when the request implies recalling or persisting state across sessions ("pick up where we left off", "anything on X before my meeting?"). Also use proactively at the start of substantive work sessions to load context and at the end to log outcomes. Do NOT use for a different owner's vault, Obsidian/REST-API development questions, "memory" meaning RAM, timed reminders, email or local-file lookups, generic second-brain advice, or notes the operator routes to a specific other file or app. --- # CHORUS Memory Use the **CHORUS** Obsidian vault as persistent memory. Read context accumulated across sessions; write things the operator asks to be remembered. The vault is shared **group memory**: one vault, many members. Each machine is configured with the **group** (who shares the vault) and this machine's **member id** (who is writing — stamped as `author:` on every agent write), in `~/.claude/chorus-memory/config.json`. Write memory in third person about people ("Jason prefers X", not "I prefer X") so the vault stays readable by every member and agent. ## API Configuration The group, member, endpoint, and API key are **machine-local** — the committed plugin ships none of them. `chorus.py` (via `chorus_config`) resolves each field from `~/.claude/chorus-memory/config.json`, with an environment override per field (`CHORUS_GROUP`, `CHORUS_MEMBER`, `CHORUS_BASE`, `CHORUS_KEY`). Never paste the key into a vault note or a doc. Exception — **per-member baked builds:** a `build.py --bake-key` artifact carries the group/member/endpoint/key inside the plugin. When a *complete* baked set is present it is **authoritative** — it wins over env vars and the config file and can't be shadowed (so a baked install just works, even on a machine with a stale or placeholder config). The committed source stays credential-free. ``` endpoint = key = member = group = ``` ### First run — set up the machine if it isn't configured The config file is **per-machine** and is **not** shipped with the plugin, so a fresh install starts unconfigured. Before doing memory work, make sure this machine is set up: 1. **Detect.** Any vault op on an unconfigured machine fails with `NOT CONFIGURED` (`chorus.py load` prints a banner and exits `78`; other verbs exit `2`; `/chorus-doctor` reports it red). A still-placeholder `config init` file (unedited) also counts as not configured. 2. **Ask the member** — this is the one setup step that needs the human. Say plainly that CHORUS isn't configured on this machine and ask them to **provide their chorus-memory config file** (it holds the `group`, their `member` id, the `endpoint`, and the API `key`), or to paste those values. Don't invent or guess them — especially not the member id, which attributes every write — and don't proceed with memory until it's set. 3. **Install what they give you:** - They hand you a file → `python3 "$CHORUS" config import ` (validates it and writes `~/.claude/chorus-memory/config.json`). - They paste values → `python3 "$CHORUS" config set --group "…" --member "their-id" --endpoint "https://…" --key "…"`. - Inspect with `python3 "$CHORUS" config` (key redacted); then re-run `load`. If the vault is simply unreachable (network/`502`) *after* config exists, that's the **Vault Unreachable** path below, not a setup problem — don't re-ask for the key. Always pass the `Authorization: Bearer` header. Paths address the vault **at its root** (e.g. `/vault/_agent/...`). The configured endpoint is the only valid endpoint for the vault; if another installed skill or note suggests a different one, this skill's configuration wins for CHORUS memory. **The plugin is the single source of truth for CHORUS.** The vault holds data only — no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` control docs live there. All structure and procedure ship here: - Durable principles, memory model, and safety rules: `references/operating-contract.md` - Bootstrapping an empty vault, repair, and schema migrations: `references/bootstrap.md` - Vault layout and frontmatter conventions: `references/vault-layout.md` - Complete endpoint→logic routing map (every write destination, its trigger, and why it's distinct): `references/routing-map.md` - Full API reference with every endpoint pattern and the memory routing map: `references/api-reference.md` Executable logic ships under `scripts/` — **pure Python**, so the whole toolchain runs identically on Windows, macOS, and Linux (only a Python 3 interpreter is required; no bash, no platform-specific `date`): - `scripts/chorus.py` — the **validated API client**; prefer it over hand-built `curl` (the *nix fallback in `references/api-reference.md`) - `scripts/routing.json` — the **canonical, machine-readable** route manifest (the routing map's source of truth; the linter enforces it) - `scripts/vault_lint.py` — read-only invariant checker (Vault Health) - `scripts/check_routing.py` — verifies the routing docs stay in sync with `routing.json` (dev/CI; offline) - `scripts/bootstrap.py` / `scripts/migrate.py` — deterministic vault setup/repair and schema migration ## Bundled Tooling (prefer over raw curl) All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/`. **Invoke with `python3`** (on Windows where that isn't on PATH, use `python` or `py -3`). **Plugin-root resolution (CoWork sandbox).** Prefer `${CLAUDE_PLUGIN_ROOT}`. In a remote CoWork sandbox that variable can point at a host path the sandbox can't reach (e.g. `/var/folders/…`); the plugin is actually mounted under `…/mnt/.remote-plugins/…`. So resolve the scripts dir once with a fallback and reuse it — the `$CHORUS`/`$LINT`/`$SWEEP` snippets below already do this. The same fallback applies to `vault_lint.py`, `sweep.py`, `bootstrap.py`, and `migrate.py`. **`scripts/chorus.py` — use this for every read/write.** It centralizes auth, **HTTP-status checking** (a failed write exits non-zero instead of looking like success), one bounded retry on 5xx/connection errors, whole-line idempotent append, correct `::` heading targets, frontmatter patches, a read-back-confirmed advisory lock, and a one-call cold-start `load`. The raw `curl` recipes in `references/api-reference.md` are the underlying mechanics / *nix fallback — reach for them only if `chorus.py` is unavailable, and if you do, **check the HTTP status yourself** (the PATCH-heading `400 invalid-target` failure silently loses writes otherwise). ```bash CHORUS="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts/chorus.py" # call as: python3 "$CHORUS" ... [ -f "$CHORUS" ] || CHORUS=$(ls /sessions/*/mnt/.remote-plugins/*/skills/chorus-memory/scripts/chorus.py 2>/dev/null | head -1) # CoWork sandbox fallback python3 "$CHORUS" load # cold-start: the 8 orientation reads in one call python3 "$CHORUS" get # 404 -> exit 44 python3 "$CHORUS" ls ; python3 "$CHORUS" map # listing / document-map python3 "$CHORUS" search python3 "$CHORUS" put # create/overwrite (read-back verified) python3 "$CHORUS" append "" # idempotent: skips only on an exact whole-line match python3 "$CHORUS" patch append heading "" python3 "$CHORUS" fm updated '"2026-06-19"' ; python3 "$CHORUS" bump # frontmatter python3 "$CHORUS" scope show ; python3 "$CHORUS" scope set "" python3 "$CHORUS" lock ; python3 "$CHORUS" unlock ``` ### High-level memory ops (prefer these — they do the routing for you) These collapse the multi-step write/recall discipline into one call each, backed by the **entity index** (`_agent/index/entities.json`, a slug→{path,kind,title,aliases} registry). **Default to them** — they are how this skill reduces the input needed to route and cross-link memory: ```bash python3 "$CHORUS" capture "" <bodyfile> --kind <kind> [--aliases a,b] [--tags t1,t2] [--source p1,p2] [--status s] [--date YYYY-MM-DD] [--domain business] python3 "$CHORUS" capture "<title>" --inbox # unknown home -> idempotent inbox line python3 "$CHORUS" resolve "<mention>" # mention -> canonical path (or where to create) python3 "$CHORUS" recall "<query>" [--json] # ranked search + 1-hop expansion -> connected context python3 "$CHORUS" link <pathA> <pathB> [--json] # add reciprocal [[Related]] links A<->B python3 "$CHORUS" triage --list --json # structured inbox listing (see Inbox Triage) ``` - **`capture` is the default write.** Given a title + `--kind`, it: resolves the entity via the index (create-vs-update, no manual search-first), derives the canonical path, stamps **complete** canonical frontmatter (`type/status/created/updated/tags/aliases/agent_written/author/source_notes` — a kind-appropriate default `status` and the kind seeded as a baseline tag, enriched by `--tags`), updates the index, **auto-links** any known entity mentioned in the body (bidirectionally), and appends today's Agent Log line — one call instead of search→put→bump→log. It also **auto-derives aliases** from the title and **learns the mention as an alias** when updating an entity under a different name. Updating an existing entity appends the **whole body** as a dated block (first line on the bullet, the rest indented under it) — nothing is truncated. `--kind` ∈ `person, company, concept, reference, meeting, project, area, semantic, episodic, working, skill, decision`. Use `--inbox` (or omit `--kind`) only when the home is genuinely unknown. - **Duplicate gate (create path).** When a new title strongly resembles an existing entity (fuzzy score ≥ 0.6, env `CHORUS_DUP_GATE`), `capture` **stops before writing** and exits `76` with the candidate list, instead of creating a near-duplicate and warning afterwards. Two precision rules (1.5.1) keep it from over-firing: the gate only blocks on a **same-kind** candidate (a decision or meeting named after a project/person is intentional naming — those surface as warnings only), and a **single shared token blocks only when it's unique to that entity** (a vault-common word like a project family name can't gate everything that mentions it; multi-token overlaps still block). Resolve a gate stop deliberately: `--merge-into <slug>` routes the capture as an update to the existing entity (the new name is learned as an alias), or `--force` creates anyway after you've confirmed they're genuinely distinct. Weak resemblances still create + warn as before. - **`resolve` before constructing any path by hand** — it returns the canonical note (matching slug/title/alias, so *Bob/Robert/RS* converge) or, when nothing matches exactly, a ranked list of **`candidates`** (entities sharing a distinctive token — e.g. "chorus memory" surfaces the project `chorus`). Always check candidates before creating a new note; a shortened name not matching exactly is the classic way a duplicate gets made. This replaces the two-search "search-first" dance for slug-addressed notes. - **`recall` for "what do we know about X"** — it returns the matching notes *and* their one-hop neighbourhood (Related links, `source_notes`), so recall surfaces the web around a topic, not an isolated note. The corpus covers the entity graph **plus session logs and journal notes** (down-weighted, so entity notes win ties) — past discussions and decisions are findable even when nobody promoted them into an entity note. Ranking fuses BM25 with **freshness** (`updated:` half-life decay) and **status** (`active` boosted, `archived` demoted), and each hit prints its `updated:`/`status:` so staleness is visible. `--json` emits structured hits (`path/score/type/updated/status/excerpt`) instead of prose. - **`link`** when two existing notes are related but not yet connected; `capture` already auto-links what it detects. The index is maintained automatically by `capture`; `sweep.py` rebuilds it and back-fills links for an existing vault (see **Vault Maintenance**). **Bootstrap / migrate** are scripts, not hand-run curl loops: `python3 scripts/bootstrap.py [--dry-run]` (idempotent, probe-before-write, never overwrites) and `python3 scripts/migrate.py [--apply]` (reads the marker's `schema_version` and applies migrations; dry-run by default). See `references/bootstrap.md`. ### Concurrency — the vault is shared, so coordinate writes CHORUS is read/written by **every member's** clients (Claude Code **and** CoWork sessions). The schema-5 member namespaces remove most contention by design — your scope/heartbeat/preferences/inbox are yours alone. What remains genuinely shared-write: the daily note's `## Agent Log` (member-tagged idempotent appends make interleaving safe), the group inbox, shared entity notes, and the machine indexes. Before a burst of writes that may overlap another member's session, take the **advisory lock**, and release it at session end: ```bash python3 "$CHORUS" lock "cc-<session-id>" # exit 75 if another session holds a fresh lock, or you lost the race # ... do the writes ... python3 "$CHORUS" unlock "cc-<session-id>" ``` Use a stable `<session-id>` for both calls (any unique string for this session). The lock is read-back-confirmed (after PUT, `chorus.py` re-reads and backs off if another writer won the race) and cooperative (a stale lock past `CHORUS_LOCK_TTL`, default 15 min, is reclaimable); it lives at `_agent/locks/vault.lock`. It is a courtesy, not a hard mutex — if you can't take it, tell the operator another session may be active rather than racing it. ### Slash commands `/chorus-load` (cold-start read), `/chorus-save <text>` (route + persist via `capture`), `/chorus-recall <query>` (recall a topic's neighbourhood), `/chorus-triage` (drain the inbox), `/chorus-health` (run the linter), `/chorus-sweep` (bring the vault up to current spec), `/chorus-reflect` (extract→preview→apply durable items from the session), `/chorus-doctor` (readiness check: config, reachability, auth, bootstrap/schema). These are explicit entry points to the procedures below. ### Session hooks (self-firing load & reflect) The plugin ships `hooks/hooks.json`, so the two most drift-prone behaviors no longer depend on remembering them: - **SessionStart** runs `chorus.py load` and injects the output as context — cold-start loading is deterministic. Still do the **reconcile** (inbox depth, scope confirm) on that injected context before working. - **Stop** fires **once per substantive session** when nothing has been reflected or session-logged yet: it blocks with a reminder to run the `/chorus-reflect` flow and write the session log + heartbeat. Reflection still previews and asks the operator before writing — the hook only ensures the question gets asked. If nothing durable emerged, just finish; never invent memories to satisfy the nudge. If the hooks are disabled or unavailable, the manual procedures below are unchanged and still authoritative. ## Operating Contract & Safety The vault is the **system of record** for long-term memory, not a scratchpad. Default to **additive updates, explicit status changes, and traceable summaries**. Keep agent-managed content (`agent_written: true` + `source_notes`) separable from human-authored content. Non-negotiable safety rules: - Do not fabricate facts, relationships, or prior decisions. - Do not mass-restructure the vault unless explicitly asked. - Do not delete notes unless deletion is explicitly requested and clearly safe. - Never store secrets or API keys inside a vault note. Full contract (principles, agent role, memory model): `references/operating-contract.md`. ## When to Load Memory Load at the start of any substantive conversation — anything beyond a single quick factual question. The signal: the operator is starting work, planning, asking for help with something that has state, or referencing prior discussions. ### Loading procedure **Run `python3 "$CHORUS" load`** — one call that performs the eight orientation reads below and prints each labeled section (404s on today's note / inboxes show as absent, not errors; an absent marker is flagged). If the vault is set up but YOUR member namespace isn't, `load` **self-onboards** it (seeds your `_agent/members/<member>/` anchors and sessions folder, and adds you to the group-profile roster). The table documents what `load` reads and why: | # | GET | Notes | |---|-----|-------| | 1 | `/vault/_agent/chorus-vault.md` | The bootstrap marker. 404 → vault not set up; follow `references/bootstrap.md`. 200 → check its `schema_version` and migrate if older. | | 2 | `/vault/_agent/memory/semantic/group-profile.md` | Shared facts about the group — what the team is, member roster, conventions | | 3 | `/vault/_agent/members/<member>/preferences.md` | THIS member's profile/preferences | | 4 | `/vault/_agent/members/<member>/current-context.md` | THIS member's active scope + Scope History | | 5 | `/vault/_agent/members/<member>/heartbeat.md` → then `/vault/_agent/sessions/<member>/` | **Read the heartbeat first** — a one-line pointer (`<session-log-path> @ <ISO-timestamp>`) written at the end of the member's previous session. It's an O(1) jump to their latest log. Fall back to the member's `sessions/` listing only if the pointer is missing or stale; then pick the ~5 most recent by reverse lex sort (filenames `YYYY-MM-DD-HHMM-<slug>.md`, so lex == chrono) and read only the relevant ones. | | 6 | `/vault/journal/daily/YYYY-MM-DD.md` | Today's SHARED note (the whole group's timeline); 404 is fine — created on first agent activity | | 7 | `/vault/_agent/members/<member>/inbox.md` | MY inbox depth probe — feeds the load-time reconcile below. 404 is fine (empty inbox). | | 8 | `/vault/inbox/captures/group.md` | GROUP inbox depth probe — unowned items any member may triage. 404 is fine. | Do not read every session log — older sessions are reachable via `POST /search/simple/?query=...` when needed. **Reconcile at load (do this every cold start, after the batch returns).** The batch already fetched everything needed for a cheap self-check — run it before diving into the work so memory maintains itself instead of drifting: 1. **Inbox depth (Inbox Triage).** If your inbox (GET #7) or the group inbox (GET #8) holds dated capture lines older than ~7 days that were never routed, surface the count once and offer to triage — see **Inbox Triage** below. This is the load-time trigger that makes triage self-firing rather than something you only run when asked. 2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — the operator runs several sessions a day across different topics, so the recorded `## Scope` is frequently stale at load. **Silently working under a stale scope is the default failure mode.** To prevent it, at load read the active scope and its freshness in one call — `python3 "$CHORUS" scope show` (prints `## Scope`, `scope_updated`, and how many sessions have been logged since) — and form a one-line judgment: *does this session's request match the recorded scope?* If it diverges, switch **before** doing the work via `python3 "$CHORUS" scope set "<new scope>"` (see **Scope Switching**). If `scope show` reports several sessions logged since the last switch, treat the recorded scope as suspect and confirm with the operator rather than trusting it. Keep the reconcile to a single short line to the operator (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request. **If a specific topic/project/person is in play**, pull its connected context in one call: ```bash python3 "$CHORUS" recall "<topic or title>" # the matching notes AND their one-hop neighbourhood ``` `recall` resolves the term against the entity index, searches, and expands one hop along `## Related` links and `source_notes` — so you get the project note *plus* its linked decisions, people, and prior sessions, not an isolated file. (If you only need the canonical path, `resolve "<title>"` is the O(1) lookup.) Do NOT narrate this loading. Reading memory is expected behavior. ## Inbox Triage (one-tap: `chorus.py triage`) There are TWO inboxes: `_agent/members/<member>/inbox.md` (YOUR catch-all for "save this somewhere — figure out where later") and `inbox/captures/group.md` (unowned group-wide items, as member-tagged lines `- YYYY-MM-DD [<member>]: …` — whoever triages first routes them). `triage --list` covers both. Without triage they grow forever and durable facts get stranded. At the start of a substantive session, check both inboxes (`load` probes them). If either holds lines older than ~7 days that haven't been routed elsewhere, surface them once: > "Three captures from last week are still in the inbox — want to route them now or leave them?" **The triage verb does the mechanical work** — it reuses the reflect pipeline (classify against the entity index → preview → apply via `capture`) and writes the processing-log audit line for every move: ```bash python3 "$CHORUS" triage --list --json # structured captures: {line, date, text, age_days} # decide a kind/title per accepted item, write a proposals JSON (reflect schema + # optional "line": the original inbox line, echoed into the audit log), then: python3 "$CHORUS" triage proposals.json # dry-run: classify + preview, writes nothing python3 "$CHORUS" triage proposals.json --apply # route via capture + log each move ``` Routing targets are the usual homes (a preference/pattern about YOU → your own `_agent/members/<member>/preferences.md` under `Preferences::Observations` via a direct PATCH; project idea → `project` with `--status incubating`; durable group fact → `semantic`; person fact → `person`). Never route another member's preference into your own preferences file — put it in theirs only if their own session confirms it, else `resources/people/`. `--apply` records each move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original line> → <destination path>`) automatically. Originals are **never deleted** unless the operator explicitly asks — the processing log is the audit trail. ## Project Lifecycle Projects move through four folders under `projects/`. The folder name and the `status:` frontmatter field MUST agree — they are two views of the same state. | Folder | `status:` | Meaning | |--------|-----------|---------| | `projects/incubating/` | `incubating` | Idea captured; not actively worked. Promote when work starts. | | `projects/active/` | `active` | Current work. The default state for anything in motion. | | `projects/on-hold/` | `on-hold` | Paused but still tracked. Resumable. | | `projects/archived/` | `archived` | Done, abandoned, or rolled into something else. Not deleted. | **Promotion / transition rule:** move the file to the new folder AND update `status:` in the same change. A file in `projects/active/` with `status: on-hold` is broken state — fix it when you see it. **Searching:** `POST /search/simple/?query=<slug>` covers all four subfolders in one call. Always search before creating a new note (see the pre-write rule below). ## When to Write Memory Write when the operator: - States a fact, preference, or commitment worth keeping ("I prefer X", "we use uv not pip", "standup is Tuesday at 10") - Makes a non-obvious decision worth recording - Says "remember that", "save this", "log this", "add to memory", "note that" - Finishes a meaningful working session future sessions should pick up Write in third person about the operator. Every note carries the canonical frontmatter (see below). Agent-generated notes set `agent_written: true` and `author: <member>` (your configured member id). **Default write path: `capture`.** For a routed memory (a fact about a person/company/project/concept/etc.), prefer `python3 "$CHORUS" capture "<title>" <bodyfile> --kind <kind>` — it does the search-first, path derivation, frontmatter, indexing, auto-linking, and Agent-Log line in one call (see **High-level memory ops**). Drop to the lower-level `put`/`patch`/`append` verbs below only for shapes `capture` doesn't model (a project `## Status` replacement, an ADR mirror, a daily-note edit) or to inspect/repair. ### Before you write — search first (MANDATORY for new notes) `capture` and `resolve` enforce this automatically via the entity index. If you write a slug-addressed note **by hand**, first **`resolve` the title/slug** (it matches aliases and returns the canonical path or confirms none exists); if `resolve` is unavailable, `search` the whole vault for the slug. Listing a single folder (e.g. `projects/active/`) is NOT sufficient — a note with the same slug may exist in `projects/on-hold/`, `projects/incubating/`, `projects/archived/`, or under a different folder entirely. ```bash python3 "$CHORUS" resolve "<title>" # preferred: alias-aware, O(1) python3 "$CHORUS" search <slug> # fallback when the index isn't trusted ``` If a match is found: - In a non-active project subfolder (`on-hold/`, `incubating/`, `archived/`): **promote/merge** — PUT the merged content to `projects/active/<slug>.md` with `status: active`, then DELETE the old location. Preserve the earliest `created:` date. - In the same folder you intended to write: **update in place** (PATCH or merged PUT). Never silently overwrite — fold the existing content in first. - Elsewhere (e.g. a stale duplicate under `resources/`): tell the operator and ask which should be canonical before writing. **Search both the slug AND any human title** the operator used (e.g. slug `chorus-memory` and title `CHORUS plugin`). Slug-only searches miss notes filed under a different naming scheme. Two cheap `POST /search/simple/?query=...` calls beat one expensive cleanup pass later. Only after the search comes back empty (or you've decided to merge) is it safe to create a new note. This rule prevents the most common duplication bug: a note exists in `on-hold/` but the agent only checked `active/` and created a parallel record. ### Before you append — read first (idempotency) `chorus.py append` is **whole-line idempotent**: it GETs the file and skips the POST when the exact line already exists, so a retry, replay, or overlapping session can't grow duplicate lines. Use it for single-line additions to `_agent/members/<member>/inbox.md`, a daily note's `## Agent Log`, or a `## Fact / Pattern` / `## Observations` / `## Log` heading. (A raw POST is **not** idempotent — if you must use curl, GET first and whole-line-match the entry yourself; see `references/api-reference.md`.) This does **not** apply to PUT or PATCH `replace`, which overwrite by design. ### Append, patch, put — via the client Write multi-line bodies to a file first (use the Write tool — cross-platform, no heredoc) and pass the path; or pipe via stdin (`-`). ```bash # additive line (idempotent). Anchor the date on the conversation's currentDate. python3 "$CHORUS" append _agent/members/<member>/inbox.md "- <currentDate>: <entry>" # targeted heading append/replace. The heading Target is the FULL `::`-delimited path # from the H1 — a bare subheading returns 400 invalid-target and the write is LOST. # If unsure of the exact path, GET the document map first and copy the heading verbatim: python3 "$CHORUS" map _agent/members/<member>/preferences.md python3 "$CHORUS" patch _agent/members/<member>/preferences.md \ append heading "Preferences::Fact / Pattern" <bodyfile> # `replace` (instead of `append`) overwrites a section entirely (e.g. a project's "Name::Status"). # create/overwrite a whole file (read-back verified; intermediate dirs auto-created) python3 "$CHORUS" put projects/active/<slug>.md <bodyfile> # frontmatter freshness after a SUBSTANTIVE change (status/decision/scope) — NOT routine log appends CHORUS_TODAY=<currentDate> python3 "$CHORUS" bump projects/active/<slug>.md # sets updated: to today # fm is CREATE-or-replace: a key the note lacks is inserted surgically (nothing else # in the file is touched), so repairing a note missing `status:` is one call: python3 "$CHORUS" fm <path> status active # search (run before creating any slug-addressed note — see the search-first rule above) python3 "$CHORUS" search <terms...> ``` Every PUT body needs the canonical YAML frontmatter (see `references/vault-layout.md`); set `agent_written: true`, `author: <your-member-id>`, and `source_notes`. Bump `updated:` on substance, not on heartbeat — adding an Agent Log line, an inbox capture, or a timestamped Observations bullet is not a meaningful content change. Raw curl mechanics for every verb live in `references/api-reference.md`. ## Scope Switching (`current-context.md`) `_agent/members/<member>/current-context.md` tracks a single active scope. The operator routinely shifts scope within a day (one project → another → docs), so this is high-churn — switch deliberately, every time the work changes topic. **Preferred — one command:** ```bash python3 "$CHORUS" scope set "<new scope summary>" ``` `scope set` does the whole switch atomically and correctly: it archives the **prior** scope to `## Scope History` (dated, truncated), replaces `## Scope` with the new text, and stamps the `scope_updated:` frontmatter timestamp. That timestamp is the **freshness signal** — it's what `chorus.py scope show` and the `vault_lint.py` drift check read to tell whether the recorded scope still reflects current work. Always switch through `scope set` so `scope_updated` stays honest; a hand-edited `## Scope` that skips the stamp reintroduces silent drift. **Manual fallback** (only if `chorus.py` is unavailable): PATCH `prepend` the prior scope to `## Scope History`, PATCH `replace` `## Scope`, then PATCH the frontmatter `scope_updated:` (and `updated:`) to today (see `references/api-reference.md` for the raw curl). Note `scope_updated` must already exist in frontmatter — a `PATCH replace` on a missing field returns `400 invalid-target`; run `bootstrap.py` to add it. This keeps a rolling trail of recent scopes in one file instead of spawning separate stash notes. Trim Scope History to the last ~10 entries when it grows past that. **Drift backstop:** `vault_lint.py` flags when ≥ `SCOPE_STALE_SESSIONS` (default 3) session logs are dated after `scope_updated` — i.e. work happened without a scope switch. It's advisory (surfaced in Vault Health / `/chorus-health`), the mechanical safety net under the load-time judgment above. ## Journal Rollups (the journal is one continuum) The journal is a single append-only chronological stream. Rollups are just coarser-grained journal entries over the same timeline, so they **all live under `journal/`** — there is no separate `reviews/` tree. One place to read the whole time-series story, daily through annual. | Cadence | Path | Trigger | Type | |---------|------|---------|------| | Daily | `journal/daily/YYYY-MM-DD.md` | first agent activity that day | `daily-note` | | Weekly | `journal/weekly/YYYY-Www.md` (e.g. `2026-W24.md`) | first substantive session of a new ISO week — **opt-in**, offer first | `weekly-note` | | Monthly | `journal/monthly/YYYY-MM.md` | first substantive session of a new calendar month — offer alongside the Vault Health pass | `monthly-note` | | Quarterly | `journal/quarterly/YYYY-Qn.md` | **manual / on request only** | `review` | | Annual | `journal/annual/YYYY.md` | **manual / on request only** | `review` | All filenames lex-sort chronologically. Derive the ISO week (`YYYY-Www`) and month (`YYYY-MM`) from the conversation's `currentDate` and check whether that period's note already exists before offering a rollup. A weekly/monthly rollup is a **light digest** — open threads across `projects/active/`, inbox items aging past ~7 days, and the period's `## Scope History` changes from `current-context.md`. It is *not* a vault-health audit (that's an agent-maintenance artifact — see below). ## Vault Health (monthly) On the first substantive session of a calendar month, run a quick health pass and write findings to `_agent/health/YYYY-MM-vault-health.md`. This is **agent self-maintenance, not a journal entry** — it lives under `_agent/` because it's about the vault's mechanical integrity, not the operator's work narrative. Don't auto-fix without asking. Run the bundled linter first — it mechanically checks the invariants below so you don't eyeball them. **Pass `CHORUS_TODAY` = the conversation's `currentDate`** so stale/aging math uses the same clock you write with (not the runner's machine date): ```bash LINT="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts/vault_lint.py" [ -f "$LINT" ] || LINT=$(ls /sessions/*/mnt/.remote-plugins/*/skills/chorus-memory/scripts/vault_lint.py 2>/dev/null | head -1) # CoWork sandbox fallback CHORUS_TODAY=<currentDate> python3 "$LINT" ``` Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Checks (the linter asserts each and prints violations): 1. **Stale active projects** — for each note in `projects/active/`, check `updated:` >30 days. Likely belongs in `on-hold/`. 2. **Unprocessed inbox** — GET `_agent/members/<member>/inbox.md`. List items older than 14 days that never moved through the triage protocol. 3. **Duplicate slugs across lifecycle folders** — any slug appearing in more than one of `active/`, `incubating/`, `on-hold/`, `archived/` is broken state. 4. **Folder ↔ `status:` mismatch** — any `projects/<lifecycle>/` note whose `status:` frontmatter disagrees with its folder. 5. **Wikilinks in frontmatter** — any `[[...]]` inside a YAML frontmatter block (breaks Obsidian reading view), swept across all folders. 6. **Duplicate `## Agent Log` headings** — any daily note with more than one. 7. **Unknown / retired paths** — any vault file that matches no route in `scripts/routing.json` (or sits at a retired path). 8. **Frontmatter integrity** — missing required fields, `updated` < `created`, future `updated`, and `[[wikilinks]]` leaking into `source_notes`. 9. **Graph health** — **broken wikilinks** (`[[X]]` resolving to no note), **orphans** (an entity note with no inbound links and an empty `## Related`), and **index drift** (entity-index entries whose file is gone, or indexable notes missing from `entities.json` — fix with `sweep.py`). 10. **Incomplete entity frontmatter** — entity notes missing or empty `status:`/`tags:` per the kind-scoped requirement map (`KIND_REQUIRED_FM` in `chorus_index.py`; journal/session notes are exempt). `capture` now stamps these at write time; `sweep.py --apply` backfills older notes (kind-default status + the kind as a baseline tag). ## Vault Maintenance — `sweep.py` (bring the vault up to spec) After a plugin upgrade, run the sweep once to back-fill what older vaults lack — it (1) creates `_agent/index/`, (2) **rebuilds the entity index** from the notes already present (and the recall index, now spanning entities + sessions/journal), (3) **backfills incomplete entity frontmatter** (kind-default `status`, the kind as a baseline tag — what `/chorus-health` flags as `incomplete-frontmatter`), (4) **symmetrizes cross-links** (for every `## Related` A→B, adds the missing B→A; it never invents new links from body text), and (5) stamps the marker `schema_version` to current. Dry-run by default: ```bash SWEEP="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts/sweep.py" [ -f "$SWEEP" ] || SWEEP=$(ls /sessions/*/mnt/.remote-plugins/*/skills/chorus-memory/scripts/sweep.py 2>/dev/null | head -1) # CoWork sandbox fallback python3 "$SWEEP" # plan (read-only) python3 "$SWEEP" --apply # write ``` Run it after `migrate.py` (which handles structural/schema changes) — or any time `/chorus-health` reports index drift. It is idempotent; re-running is safe. The pass is cheap and pays for itself by catching drift before it requires a reorg. Write the findings as a digest; act on them only with the operator's go-ahead. **Performance.** Full-vault scripts (`sweep.py`, `vault_lint.py`, recall rebuild) are connection-pooled (keep-alive) and read the whole vault concurrently via `chorus.read_many`, so they finish in about a second on a few-hundred-note vault instead of timing out on hundreds of serial TLS handshakes. Tune with `CHORUS_WORKERS` (default 8, bulk-read concurrency) and `CHORUS_TIMEOUT` (default 30s, per-request). For a vault large enough that even the concurrent pass nears the tool timeout, run the script with the harness's background-execution option rather than blocking the turn. ## Daily Note — Agent Log After substantive activity, write a one-line entry to today's daily note's `## Agent Log` heading. The daily note is the **group's shared timeline** — every member's sessions append to the same heading — so **every line is member-tagged**: `- YYYY-MM-DD [<member>]: <what happened>`. (`capture` writes its log line in this form automatically.) The daily-note **template** (`journal/templates/daily-note-template.md`) defines this heading, but ad-hoc daily notes created without the template don't have it — so PATCH can fail with `invalid-target` if the note exists but lacks the heading. **Procedure (resilient)** — all dates are the conversation's `currentDate`, not the machine clock: 1. `python3 "$CHORUS" get journal/daily/<currentDate>.md`. 2. **404** — fetch `journal/templates/daily-note-template.md`, substitute its `{{date:YYYY-MM-DD}}` tokens to `<currentDate>`, write the result to a file (Write tool), and `put` it to the daily path; then go to step 4. 3. **200 but no `## Agent Log`** — detect by matching an anchored `^## Agent Log` line in the **raw markdown** you just fetched. Do **not** check the document map, whose headings are full `::`-delimited paths (e.g. `2026-06-07::Agent Log`), so a bare `Agent Log` never matches and you'd append a duplicate heading. If missing, `post` a body of exactly `\n\n## Agent Log\n` to add the heading. 4. PATCH-append the entry under `<currentDate>::Agent Log` (the daily note's H1 is the date, so that is the full target path): ```bash python3 "$CHORUS" patch journal/daily/<currentDate>.md append heading "<currentDate>::Agent Log" <bodyfile> ``` ## Where to Write | Situation | File / path | Method | |-----------|-------------|--------| | Unsure where it goes / quick capture (mine) | `_agent/members/<member>/inbox.md` (date-prefixed line) | POST | | Unowned group-wide capture ("someone should look at this") | `inbox/captures/group.md` (member-tagged line `- YYYY-MM-DD [<member>]: …`) | POST | | MY preference or personal pattern | `_agent/members/<member>/preferences.md` (append under the right heading) | PATCH | | Durable group facts / patterns (incl. `group-profile.md`) | `_agent/memory/semantic/<slug>.md` | PUT | | What happened (event record) | `_agent/memory/episodic/<slug>.md` | PUT | | Short-lived working state | `_agent/memory/working/<slug>.md` | PUT | | Task-scoped context / focus | `_agent/members/<member>/current-context.md` | PATCH / PUT | | Working session ended with substance | `_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md` | PUT | | Long-running project state | `projects/<lifecycle>/<slug>.md` (see Project Lifecycle) | PUT + PATCH | | Ongoing area of responsibility (standing domain, no end state) | `areas/<domain>/<slug>.md` (domain: `business`/`personal`/`learning`/`systems`) | PUT | | Non-obvious decision (ADR) | `decisions/by-date/YYYY-MM-DD-<slug>.md` (see mirror note below) | PUT | | Person context | `resources/people/<name>.md` | PUT / PATCH | | Company / organization context | `resources/companies/<slug>.md` | PUT / PATCH | | Concept / reference note | `resources/concepts/` or `resources/references/` | PUT | | Meeting notes / call recap | `resources/meetings/YYYY-MM-DD-<slug>.md` | PUT | | Skill / plugin capability entry (catalog, not the build work) | `_agent/skills/active/<slug>.md` (→ `_agent/skills/archived/` when retired) | PUT | | Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` — see **Daily Note — Agent Log** above | PATCH (with auto-create) | | Weekly / monthly rollup | `journal/weekly/YYYY-Www.md` · `journal/monthly/YYYY-MM.md` — see **Journal Rollups** | PUT | | Quarterly / annual review | `journal/quarterly/YYYY-Qn.md` · `journal/annual/YYYY.md` (manual / on request) | PUT | | Vault-health audit (agent self-maintenance) | `_agent/health/YYYY-MM-vault-health.md` — see **Vault Health** | PUT | | Session-end orientation pointer | `_agent/members/<member>/heartbeat.md` (one line: `<session-log-path> @ <ISO-timestamp>`) | PUT | > **The complete, audited routing map lives in `references/routing-map.md`** — every write destination with its trigger, what lands there, and why it's distinct from its neighbors. This table is the quick-reference; the map is the authority. If a path isn't in the map, it shouldn't be written to. **Decision mirrors:** if the decision belongs to an existing note in `projects/active/`, add a `[[wikilink]]` to the ADR under that project's `## Key Decisions` heading (PATCH). If no matching project note exists, skip the mirror — the by-date ADR is sufficient; do not invent topical mirror files in `decisions/by-project/`. **Routing triggers — areas / meetings / skills:** - **Area** (`areas/`): a standing responsibility the operator maintains with no finish line. Decision rule: has an end state → `projects/`; never "done" → `areas/`. Subfolder is the domain (`business`/`personal`/`learning`/`systems`); create it on first write. `type: area`. - **Meeting** (`resources/meetings/`): notes, recaps, or action items tied to a specific meeting or call. Mirror decisions to `decisions/by-date/` and commitments to the relevant project. `type: meeting`. - **Skill** (`_agent/skills/`): the operator authors, installs, or retires a skill/plugin and wants it tracked as a capability — distinct from `projects/`, which tracks the build effort. Active entries in `active/`, retired ones in `archived/`. `type: skill`. Never delete files unless the operator explicitly asks. Memory is append-friendly; deletion is destructive. ## Session Logging At the end of substantive conversations (ones that produced decisions, artifacts, or commitments), create a session log. Ask once if unsure: "Want me to log a session note for this?" **Filename format is canonical: `_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md`.** Always include the four-digit local-time HHMM component — it makes filenames lexically sort in true chronological order, which is what Step 3 of loading relies on. Older session logs without the HHMM part exist; leave them alone, but every new one must use the full form. Write the body (see `references/session-log-template.md`) to a file with the Write tool, then: ```bash python3 "$CHORUS" put _agent/sessions/<currentDate>-HHMM-<slug>.md <bodyfile> ``` Then add a one-line entry to today's daily note via the **Daily Note — Agent Log** procedure above. Finally, update the heartbeat pointer so the next session can orient in one read (this closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read). It is a single line, `<session-log-path> @ <ISO-timestamp>`; write it to a file and PUT it: ```bash python3 "$CHORUS" put _agent/members/<member>/heartbeat.md <bodyfile> ``` `last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate. ## Vault Unreachable If the API returns a connection error, timeout, or `502`, tell the operator once that the memory vault is unreachable (a `502` usually means Obsidian/the REST plugin is not running on the backend), then proceed without memory. Do not retry repeatedly. ## Style Rules - Write in third person about the operator: "the operator prefers X", not "I prefer X". - This vault holds the GROUP's shared memory — every member reads and writes it, and everything in it is visible to the whole group. Do not edit another member's `_agent/members/<id>/` namespace (their preferences/scope/heartbeat/inbox belong to their sessions); facts about them as a person go in `resources/people/`. Do not cross-write with vaults outside this group. - **Anchor relative dates on the conversation's `currentDate`** before writing. "Today" → `currentDate`. "Thursday" / "next week" → resolve to an absolute `YYYY-MM-DD`. Never guess from training-data knowledge of the current year. - Every memory file has canonical YAML frontmatter — see `references/vault-layout.md`. - Set `agent_written: true` and `author: <your-member-id>` on agent-generated notes and list `source_notes` (plain relative paths, not links). - **`created:` is the earliest known date the entity was tracked anywhere in the vault, not "today".** When merging notes (e.g. promoting `on-hold/` → `active/`), preserve the earliest `created:` and only bump `updated:`. - **`source_notes` lists the note(s) that *triggered* or *supplied content for* this one** — e.g. the session log that produced a project update, or the daily note where a captured fact originated. It is a *backward* link to inputs. Forward links (this note → other notes it references) belong in the `## Related` section in the note body, never in frontmatter. - **Never put `[[wikilinks]]` in frontmatter** — YAML parses them as nested lists and the links break in Obsidian's reading view. Put all cross-references in a `## Related` section in the note **body** as a bulleted list of `[[links]]`. - Use Obsidian wiki links (`[[note name]]`) freely in the note **body** for cross-references. - Keep entries short and focused. Fewer, sharper entries beat many noisy ones — the operator explicitly prefers concision. - About to write something large or sensitive? Show the operator the content first and confirm. ## operator-preferences.md — Rules vs Observations `_agent/members/<member>/preferences.md` separates two kinds of content: - `## Fact / Pattern` — **promoted, deduped rules.** No date prefix. These are timeless: "the operator prefers concise communication." Append here only when a rule is stable. - `## Observations` — **timestamped raw observations.** Date-prefixed: `- 2026-06-06: the operator chose X over Y because Z.` This is where new evidence goes by default. During monthly Vault Health or when an observation stabilizes, promote it from `## Observations` into `## Fact / Pattern` (drop the date) and remove the duplicate from Observations. Trim Observations to the last ~30 entries when it grows past that — the rest live in session logs. ## What This Skill Does Not Do - Does not replace reasoning. The vault is reference material — apply judgment. - Does not auto-summarize the whole vault. Read targeted files, not everything. - Does not store passwords or secrets, and never writes the API key into a vault note. If asked to "remember my password is X", decline and suggest a password manager.