1
0
forked from jason/echo
Files
chorus/chorus-memory.plugin.src/skills/chorus-memory/SKILL.md
T

460 lines
47 KiB
Markdown
Raw Normal View History

---
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.
2026-06-26 15:39:17 -05:00
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>
```
2026-06-23 22:17:39 -05:00
### First run — set up the machine if it isn't configured
2026-06-22 09:27:36 -05:00
2026-06-23 22:17:39 -05:00
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.
2026-06-23 22:17:39 -05:00
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`.
2026-06-23 22:17:39 -05:00
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:
2026-06-07 01:18:02 -05:00
- 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`
2026-06-11 10:57:01 -05:00
- Complete endpoint→logic routing map (every write destination, its trigger, and why it's distinct): `references/routing-map.md`
2026-06-07 01:18:02 -05:00
- Full API reference with every endpoint pattern and the memory routing map: `references/api-reference.md`
2026-06-21 11:46:54 -05:00
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`):
2026-06-19 21:12:14 -05:00
- `scripts/chorus.py` — the **validated API client**; prefer it over hand-built `curl` (the *nix fallback in `references/api-reference.md`)
2026-06-19 21:12:14 -05:00
- `scripts/routing.json` — the **canonical, machine-readable** route manifest (the routing map's source of truth; the linter enforces it)
2026-06-21 11:46:54 -05:00
- `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
2026-06-19 21:12:14 -05:00
## 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`).
2026-06-19 21:12:14 -05:00
**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`.
2026-06-26 20:05:47 -05:00
**`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).
2026-06-19 21:12:14 -05:00
```bash
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 # cold-start: the 8 orientation reads in one call
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>
2026-06-19 21:12:14 -05:00
```
2026-06-21 11:46:54 -05:00
### 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 "<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)
2026-06-21 11:46:54 -05:00
```
- **`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.
2026-06-21 11:46:54 -05:00
- **`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`.
2026-06-19 21:12:14 -05:00
### 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:
2026-06-19 21:12:14 -05:00
```bash
python3 "$CHORUS" lock "cc-<session-id>" # exit 75 if another session holds a fresh lock, or you lost the race
2026-06-19 21:12:14 -05:00
# ... do the writes ...
python3 "$CHORUS" unlock "cc-<session-id>"
2026-06-19 21:12:14 -05:00
```
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.
2026-06-19 21:12:14 -05:00
### 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.
2026-06-19 21:12:14 -05:00
### 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.
2026-06-07 01:18:02 -05:00
## 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
2026-06-23 22:17:39 -05:00
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:
2026-06-06 23:34:55 -05:00
| # | 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. |
2026-06-06 23:34:55 -05:00
Do not read every session log — older sessions are reachable via `POST /search/simple/?query=...` when needed.
2026-06-11 10:57:01 -05:00
**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.
2026-06-11 10:57:01 -05:00
2026-06-23 22:17:39 -05:00
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.
2026-06-11 10:57:01 -05:00
2026-06-21 11:46:54 -05:00
**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
```
2026-06-21 11:46:54 -05:00
`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.)
2026-06-06 23:34:55 -05:00
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:
2026-06-06 23:34:55 -05:00
> "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
2026-06-23 22:17:39 -05:00
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.
2026-06-21 11:46:54 -05:00
### Before you write — search first (MANDATORY for new notes)
2026-06-21 11:46:54 -05:00
`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.
2026-06-23 22:17:39 -05:00
- 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.
2026-06-06 23:34:55 -05:00
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.
2026-06-06 23:34:55 -05:00
### 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.
2026-06-06 23:34:55 -05:00
2026-06-21 11:46:54 -05:00
### Append, patch, put — via the client
2026-06-21 11:46:54 -05:00
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
2026-06-21 11:46:54 -05:00
# additive line (idempotent). Anchor the date on the conversation's currentDate.
python3 "$CHORUS" append _agent/members/<member>/inbox.md "- <currentDate>: <entry>"
2026-06-21 11:46:54 -05:00
# 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>
2026-06-21 11:46:54 -05:00
# `replace` (instead of `append`) overwrites a section entirely (e.g. a project's "Name::Status").
2026-06-06 23:34:55 -05:00
2026-06-21 11:46:54 -05:00
# create/overwrite a whole file (read-back verified; intermediate dirs auto-created)
python3 "$CHORUS" put projects/active/<slug>.md <bodyfile>
2026-06-06 23:34:55 -05:00
2026-06-21 11:46:54 -05:00
# 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
2026-06-21 11:46:54 -05:00
# 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`.
2026-06-06 23:34:55 -05:00
## 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.
2026-06-06 23:34:55 -05:00
2026-06-19 21:57:41 -05:00
**Preferred — one command:**
2026-06-06 23:34:55 -05:00
2026-06-19 21:57:41 -05:00
```bash
python3 "$CHORUS" scope set "<new scope summary>"
2026-06-19 21:57:41 -05:00
```
`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.
2026-06-19 21:57:41 -05:00
**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.
2026-06-06 23:34:55 -05:00
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.
2026-06-19 21:57:41 -05:00
2026-06-11 10:57:01 -05:00
## 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.
2026-06-11 10:57:01 -05:00
| 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` |
2026-06-21 11:46:54 -05:00
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.
2026-06-11 10:57:01 -05:00
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).
2026-06-06 23:34:55 -05:00
## Vault Health (monthly)
2026-06-23 22:17:39 -05:00
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.
2026-06-11 10:57:01 -05:00
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):
2026-06-06 23:34:55 -05:00
2026-06-11 10:57:01 -05:00
```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"
2026-06-11 10:57:01 -05:00
```
2026-06-19 21:12:14 -05:00
Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Checks (the linter asserts each and prints violations):
2026-06-06 23:34:55 -05:00
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.
2026-06-06 23:34:55 -05:00
3. **Duplicate slugs across lifecycle folders** — any slug appearing in more than one of `active/`, `incubating/`, `on-hold/`, `archived/` is broken state.
2026-06-11 10:57:01 -05:00
4. **Folder ↔ `status:` mismatch** — any `projects/<lifecycle>/` note whose `status:` frontmatter disagrees with its folder.
2026-06-19 21:12:14 -05:00
5. **Wikilinks in frontmatter** — any `[[...]]` inside a YAML frontmatter block (breaks Obsidian reading view), swept across all folders.
2026-06-11 10:57:01 -05:00
6. **Duplicate `## Agent Log` headings** — any daily note with more than one.
2026-06-19 21:12:14 -05:00
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`.
2026-06-21 11:46:54 -05:00
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).
2026-06-21 11:46:54 -05:00
## 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:
2026-06-21 11:46:54 -05:00
```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
2026-06-26 20:05:47 -05:00
python3 "$SWEEP" # plan (read-only)
python3 "$SWEEP" --apply # write
2026-06-21 11:46:54 -05:00
```
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.
2026-06-06 23:34:55 -05:00
2026-06-23 22:17:39 -05:00
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.
2026-06-06 23:34:55 -05:00
**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.
2026-06-22 23:55:19 -05:00
## 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.
2026-06-21 11:46:54 -05:00
**Procedure (resilient)** — all dates are the conversation's `currentDate`, not the machine clock:
1. `python3 "$CHORUS" get journal/daily/<currentDate>.md`.
2026-06-21 11:46:54 -05:00
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 |
2026-06-11 14:02:59 -05:00
| 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) |
2026-06-11 10:57:01 -05:00
| 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 |
2026-06-11 10:57:01 -05:00
> **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:**
2026-06-23 22:17:39 -05:00
- **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`.
2026-06-23 22:17:39 -05:00
- **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`.
2026-06-23 22:17:39 -05:00
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.
2026-06-21 11:46:54 -05:00
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.
2026-06-21 11:46:54 -05:00
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:
2026-06-11 10:57:01 -05:00
```bash
python3 "$CHORUS" put _agent/members/<member>/heartbeat.md <bodyfile>
2026-06-11 10:57:01 -05:00
```
2026-06-21 11:46:54 -05:00
`last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate.
2026-06-11 10:57:01 -05:00
## Vault Unreachable
2026-06-23 22:17:39 -05:00
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
2026-06-23 22:17:39 -05:00
- 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.
2026-06-06 23:34:55 -05:00
- **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.
2026-06-23 22:17:39 -05:00
- 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.
2026-06-06 23:34:55 -05:00
## operator-preferences.md — Rules vs Observations
`_agent/members/<member>/preferences.md` separates two kinds of content:
2026-06-06 23:34:55 -05:00
2026-06-23 22:17:39 -05:00
- `## 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.
2026-06-06 23:34:55 -05:00
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.