1
0
forked from jason/echo
Files
chorus/chorus-memory.plugin.src/skills/chorus-memory/SKILL.md
T
jason 2ddf199850 Phase 4: shared-write hardening + group lenses
- vault_lock gains bounded retry/backoff (CHORUS_LOCK_RETRIES=4,
  CHORUS_LOCK_BACKOFF=0.5s); atomic_index_update and recall-index upkeep
  retry acquisition — unlocked fallback only after exhausted retries,
  loudly, so an already-written note is never stranded unindexed.
- Duplicate-gate candidates carry the existing note's author; STOP
  message says whose entity the capture collided with.
- recall --author <member> filters hits + linked context to one member's
  notes; recall briefs and --json surface the author: stamp.
- load --all-members appends a group-wide view (every member's scope +
  last-session heartbeat); roster parsing accepts folders as
  trailing-slash file entries (the REST API's actual shape).
- operating-contract.md concurrency section rewritten for N members.
- Two-member e2e suite committed as eval/test_multimember.py (26 checks
  incl. all four Phase-4 behaviors).
- Manifest -> 2.0.0-alpha.4; rebuilt chorus-memory.plugin.

Verified: 25/25 unit, scaffold, routing-sync, 5 mock e2e suites (incl.
new multimember), run_eval metrics unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:29:55 -05:00

47 KiB

name, description
name description
chorus-memory 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 = <config "endpoint" / $CHORUS_BASE>
key      = <config "key" / $CHORUS_KEY — resolved at runtime, never stored in the plugin>
member   = <config "member" / $CHORUS_MEMBER — REQUIRED: this member's kebab-case id, stamped as author: on writes>
group    = <config "group" / $CHORUS_GROUP — display name for the group sharing the vault>

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 <path-to-their-file> (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).

CHORUS="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts/chorus.py"   # call as: python3 "$CHORUS" <cmd> ...
[ -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 [--all-members]         # cold-start: the 8 orientation reads (+ optional every-member scope/last-session view)
python3 "$CHORUS" get   <path>                  # 404 -> exit 44
python3 "$CHORUS" ls    <dir>   ;  python3 "$CHORUS" map <path>    # listing / document-map
python3 "$CHORUS" search <terms...>
python3 "$CHORUS" put    <path> <file>          # create/overwrite (read-back verified)
python3 "$CHORUS" append <path> "<line>"        # idempotent: skips only on an exact whole-line match
python3 "$CHORUS" patch  <path> append heading "<H1::Sub>" <file>
python3 "$CHORUS" fm     <path> updated '"2026-06-19"'   ;  python3 "$CHORUS" bump <path>   # frontmatter
python3 "$CHORUS" scope  show   ;  python3 "$CHORUS" scope set "<text>"
python3 "$CHORUS" lock   <session-id>  ;  python3 "$CHORUS" unlock <session-id>

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:

python3 "$CHORUS" capture "<title>" <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. --kindperson, 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:

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:

python3 "$CHORUS" recall "<topic or title>" [--author <member>]   # matching notes + one-hop neighbourhood (optionally one member's only)

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:

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.

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 (-).

# 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:

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):

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 healthbroken 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:

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):
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:

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:

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 / Patternpromoted, deduped rules. No date prefix. These are timeless: "the operator prefers concise communication." Append here only when a rule is stable.
  • ## Observationstimestamped 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.