forked from jason/echo
Phase 2: identity plumbing — group/member config, author: attribution
Config schema {owner,endpoint,key} -> {group, member, endpoint, key}:
- member (kebab-case slug, validated) is REQUIRED — who this machine
writes as; group is the descriptive team name. CHORUS_GROUP/
CHORUS_MEMBER env, config set --group/--member, doctor + config show
both with sources, NOT-CONFIGURED (78) without a valid member.
- capture stamps author: <member> on every note (_build_note); bootstrap
gains {{MEMBER}} substitution and stamps the seeded anchors; all 8
scaffold templates + canonical frontmatter docs gain author:.
- New missing-author lint check: agent_written notes must carry author:
(bootstrap marker exempt — plugin-owned).
- Lock owner is now <member>-<client>-<pid> (auto_owner); offline-queue
records carry a member field for audit.
- build.py --bake-key bakes group/member/endpoint/key (--group/--member;
member required + slug-validated) for per-member artifacts.
- Manifest -> 2.0.0-alpha.2; rebuilt chorus-memory.plugin.
Verified: 25/25 unit (+config/member checks), scaffold suite (+6 new
member/config assertions), routing-sync, 4 mock e2e (+capture-stamps-
author), run_eval metrics unchanged, +8 phase-2 smoke checks green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,18 +7,19 @@ description: Use the CHORUS Obsidian vault as the operator's persistent memory a
|
||||
|
||||
Use the **CHORUS** Obsidian vault as persistent memory. Read context accumulated across sessions; write things the operator asks to be remembered.
|
||||
|
||||
The vault belongs to a single **owner**, configured per machine (the `owner` field in `~/.claude/chorus-memory/config.json`) — this is that person's personal memory substrate. Write memory in third person about the owner ("the operator prefers X", not "I prefer X") so the vault stays readable by humans and other agents.
|
||||
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 owner, 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_OWNER`, `CHORUS_BASE`, `CHORUS_KEY`). Never paste the key into a vault note or a doc.
|
||||
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-user baked builds:** a `build.py --bake-key` artifact carries the owner/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.
|
||||
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>
|
||||
owner = <config "owner" / $CHORUS_OWNER — how to refer to the vault owner, third person>
|
||||
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
|
||||
@@ -26,10 +27,10 @@ owner = <config "owner" / $CHORUS_OWNER — how to refer to the vault owner,
|
||||
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 operator** — 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 vault `owner`, `endpoint`, and API `key`), or to paste those three values. Don't invent or guess them, and don't proceed with memory until it's set.
|
||||
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 --owner "…" --endpoint "https://…" --key "…"`.
|
||||
- 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.
|
||||
@@ -88,7 +89,7 @@ python3 "$CHORUS" link <pathA> <pathB> [--json] # add reciprocal [[Relate
|
||||
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/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.
|
||||
- **`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.
|
||||
@@ -214,7 +215,7 @@ Write when the operator:
|
||||
- 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`.
|
||||
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.
|
||||
|
||||
@@ -269,7 +270,7 @@ python3 "$CHORUS" fm <path> status active
|
||||
python3 "$CHORUS" search <terms...>
|
||||
```
|
||||
|
||||
Every PUT body needs the canonical YAML frontmatter (see `references/vault-layout.md`); set `agent_written: true` 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`.
|
||||
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`)
|
||||
|
||||
@@ -431,7 +432,7 @@ If the API returns a connection error, timeout, or `502`, tell the operator once
|
||||
- This vault holds only its own owner's memory. Do not cross-write between distinct vaults/owners — another person's vault and preferences belong in their own vault, not here.
|
||||
- **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` on agent-generated notes and list `source_notes` (plain relative paths, not links).
|
||||
- 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]]`.
|
||||
|
||||
Reference in New Issue
Block a user