1
0
forked from jason/echo

Phase 1: mechanical echo→chorus rename with back-compat shims

Identity rename, no behavior change (CHORUS-PLAN.md Phase 1):
- Plugin echo-memory → chorus-memory: manifest (v2.0.0-alpha.1), skill dir,
  16 scripts (chorus.py, chorus_config.py, …), EchoError → ChorusError,
  /chorus-* commands, hook paths, docs, scaffold seeds, eval harness,
  build.py. Docs endpoint → chorusapi.mpm.to.
- Env ECHO_* → CHORUS_*; config → ~/.claude/chorus-memory/config.json;
  state dir → ~/.chorus-memory/; marker → _agent/chorus-vault.md.

Back-compat shims (one major version):
- chorus_config aliases legacy ECHO_* env at import; reads a legacy
  echo-memory config.json when no CHORUS config exists (writes never
  land there); doctor/config report the legacy source.
- State dir honors an existing ~/.echo-memory when the new dir is absent
  (offline queue not stranded).
- Marker dual-probe in load/doctor/bootstrap/lint/sweep/migrate: a
  pre-fork _agent/echo-vault.md counts as bootstrapped; bootstrap repair
  won't write a second marker; routing gains agent-marker-legacy (GET).

Verified: 25/25 unit tests, scaffold + routing-sync checks, 4 mock e2e
suites, run_eval metrics unchanged, +6 shim smoke tests green.
Rebuilt chorus-memory.plugin (79 entries).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 13:35:51 -05:00
parent ff2f2efd0b
commit d366ca4032
98 changed files with 1312 additions and 1164 deletions
@@ -0,0 +1,244 @@
# CHORUS — Obsidian Local REST API Reference
Server: the configured endpoint — the `endpoint` from `~/.claude/chorus-memory/config.json` (overridable with `CHORUS_BASE`), a reverse proxy → backend Obsidian Local REST API. Examples below use `$CHORUS_BASE` (or the neutral placeholder `https://obsidian.example.com`).
Auth header: `Authorization: Bearer $CHORUS_KEY``export CHORUS_KEY=<token>` before running these recipes, or let `chorus.py` resolve it automatically (per-field, first wins: env override `CHORUS_KEY` → the `key` in `~/.claude/chorus-memory/config.json`, resolved via the `chorus_config` module). The literal key is never stored in docs.
A configured endpoint should present a **valid TLS certificate**`-k` is not required. Paths address the vault at its **root**.
> **Prefer `scripts/chorus.py` over the raw recipes below.** It wraps every verb with auth, status checking, retry, idempotent append, and frontmatter patches, and runs on any platform with a Python interpreter. The `curl` recipes here are the underlying mechanics and a **\*nix/bash fallback** (they use heredocs and `--data-binary`); on Windows, prefer `chorus.py` or an equivalent. **If you call `curl` directly, check the HTTP status** — add `-o /dev/null -w "%{http_code}"` and branch on it. A `PATCH` to a non-existent heading returns `400 invalid-target` (errorCode 40080) and the write is *silently lost*; a bare `curl` that ignores status will report success anyway. `GET` returns `404` for a missing file. Treat any `>= 400` as a failed operation, surface it, and do not continue as if it succeeded.
---
## Client performance (chorus.py) — pooling & concurrency
`chorus.request()` keeps **one persistent keep-alive connection per thread** and reuses it across calls, instead of opening a fresh TCP+TLS connection per request. Against a remote HTTPS endpoint the repeated handshake was the dominant cost, so a full-vault pass used to make hundreds of serial handshakes and exceed the agent/tool timeout (which kills the run mid-flight). A stale pooled connection reconnects transparently; the `(status, body)` contract is unchanged (status `0` still means transport failure, so the offline write-queue still triggers).
For bulk reads, **`chorus.read_many(paths)`** fans GETs across a thread pool and returns `{path: text_or_None}` — resilient (one unreadable file becomes `None` rather than aborting the batch). `sweep.py` and `vault_lint.py` prefetch the whole vault once with it and share that single cache across every pass, so no note is fetched more than once. Net effect: a several-minute full-vault sweep becomes sub-second on a few-hundred-note vault.
Tuning knobs (env overrides):
- `CHORUS_TIMEOUT` (default `30`) — per-request socket timeout in seconds. With concurrency, one slow file only blocks its own worker, not the run.
- `CHORUS_WORKERS` (default `8`) — `read_many` thread-pool size. Raise it for very large vaults; lower it to be gentler on the backend.
For a vault large enough that even the concurrent pass approaches the tool timeout, run the script with the harness's background-execution option rather than blocking the turn.
---
## Reading Files
```bash
# Read any file by vault path
curl -s \
-H "Authorization: Bearer $CHORUS_KEY" \
"$CHORUS_BASE/vault/_agent/context/current-context.md"
```
Returns raw file content (text/markdown). On 404, the file does not exist.
```bash
# Read a specific heading's content only
curl -s \
-H "Authorization: Bearer $CHORUS_KEY" \
"$CHORUS_BASE/vault/_agent/memory/semantic/operator-preferences.md/heading/Operator"
```
Nested headings: separate levels with `::` (URL-encode spaces as `%20`):
```
/vault/path/to/note.md/heading/Work%3A%3AMeetings
```
---
## Listing Directories
```bash
# List contents of a directory (trailing slash required)
curl -s \
-H "Authorization: Bearer $CHORUS_KEY" \
"$CHORUS_BASE/vault/_agent/sessions/"
```
Returns JSON: `{ "files": [...], "folders": [...] }`.
---
## Appending Content (POST)
`POST` appends to the **end** of an existing file. Creates the file if it doesn't exist.
```bash
cat > /tmp/obs_entry.md << 'OBSEOF'
- 2026-06-05: your entry here
OBSEOF
curl -s -X POST \
-H "Authorization: Bearer $CHORUS_KEY" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_entry.md \
"$CHORUS_BASE/vault/inbox/captures/inbox.md"
```
---
## Creating or Overwriting Files (PUT)
`PUT` creates a new file or fully overwrites an existing one. Intermediate directories are created automatically.
```bash
cat > /tmp/obs_file.md << 'OBSEOF'
---
type: session-log
status: complete
created: 2026-06-05
updated: 2026-06-05
tags: [agent, session]
agent_written: true
source_notes: []
---
# content here
## Related
- [[projects/active/some-project]]
OBSEOF
curl -s -X PUT \
-H "Authorization: Bearer $CHORUS_KEY" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_file.md \
"$CHORUS_BASE/vault/_agent/sessions/2026-06-05-1430-my-session.md"
```
---
## Patching a Specific Section (PATCH)
`PATCH` edits inside a file without rewriting it. Target and operation are set via headers.
### Append under a heading
**Heading targets must be the FULL heading path, `::`-delimited from the top-level heading.** A bare subheading name returns `400 invalid-target` (errorCode 40080). Example: `## Fact / Pattern` nested under `# Operator Preferences``Target: Operator Preferences::Fact / Pattern`. Percent-encode non-ASCII characters (e.g. `H%C3%A9llo`); spaces are fine.
```bash
cat > /tmp/obs_patch.md << 'OBSEOF'
The operator prefers concise status updates — lead with the decision.
OBSEOF
curl -s -X PATCH \
-H "Authorization: Bearer $CHORUS_KEY" \
-H "Operation: append" \
-H "Target-Type: heading" \
-H "Target: Operator Preferences::Fact / Pattern" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_patch.md \
"$CHORUS_BASE/vault/_agent/memory/semantic/operator-preferences.md"
```
### Discover heading / block / frontmatter targets
When unsure of the exact heading path, GET the note with the document-map Accept header:
```bash
curl -s \
-H "Authorization: Bearer $CHORUS_KEY" \
-H "Accept: application/vnd.olrapi.document-map+json" \
"$CHORUS_BASE/vault/_agent/memory/semantic/operator-preferences.md"
```
Returns `{ "headings": [...], "blocks": [...], "frontmatterFields": [...] }`. Copy the heading string verbatim into `Target`.
### Replace a heading's content entirely
Same call with `Operation: replace` — e.g. to refresh a project's `Project Name::Status`.
### Prepend under a heading
Same call with `Operation: prepend`.
### Patch a frontmatter field
> A `PATCH` `Target-Type: frontmatter` **replace** on a key the note does not have returns
> `400 invalid-target` — the raw API cannot create a key. `chorus.py fm` (v1.5+) handles this:
> on that 400 it surgically inserts the one `field: value` line into the frontmatter block
> and re-PUTs (verified), leaving every other line untouched. Raw-curl users must GET-edit-PUT
> themselves — carefully (a naive rewrite is how frontmatter gets clobbered).
```bash
curl -s -X PATCH \
-H "Authorization: Bearer $CHORUS_KEY" \
-H "Operation: replace" \
-H "Target-Type: frontmatter" \
-H "Target: updated" \
-H "Content-Type: application/json" \
--data '"2026-06-05"' \
"$CHORUS_BASE/vault/projects/active/vault-foundation.md"
```
---
## Searching
```bash
curl -s -X POST \
-H "Authorization: Bearer $CHORUS_KEY" \
"$CHORUS_BASE/search/simple/?query=weekly+review"
```
Returns an array of `{ filename, score, matches: [{ context, match }] }`.
---
## Deleting Files
```bash
curl -s -X DELETE \
-H "Authorization: Bearer $CHORUS_KEY" \
"$CHORUS_BASE/vault/inbox/imports/old-note.md"
```
Only on explicit operator request. Deletion is destructive.
> **DELETE removes files only — never directories.** Deleting the last file in a folder
> leaves the empty folder orphaned on disk; the REST listing then 404s (nothing left to
> list), which falsely reads as "the tree is gone", but Obsidian's file explorer still
> shows it. After removing a folder's last file, tell the operator the empty folder
> needs manual deletion in Obsidian — do not claim the tree is gone from a 404 listing.
> (Learned 2026-07-03 removing the retired `archive/` and `_agent/outputs/` trees.)
---
## URL-Encoding Notes
- Path separators (`/`) in the vault path are **literal** — do not encode them.
- Spaces in filenames or heading targets in a URL: use `%20`.
- Nested heading levels in a URL path: use `%3A%3A` for `::`.
- Heading text in the `Target:` header: the **full heading path** joined by `::` (e.g. `Operator Preferences::Fact / Pattern`), no `#`; spaces are fine; percent-encode non-ASCII.
---
## Memory Routing Map
| Situation | Vault path | Method |
|-----------|-----------|--------|
| Quick capture / unsorted | `inbox/captures/inbox.md` (date-prefixed line) | POST |
| Raw imported material | `inbox/imports/` | PUT |
| Operator preference / durable fact | `_agent/memory/semantic/operator-preferences.md` | PATCH |
| Other durable facts, patterns | `_agent/memory/semantic/<slug>.md` | PUT |
| Event record (what happened) | `_agent/memory/episodic/<slug>.md` | PUT |
| Short-lived, time-boxed state | `_agent/memory/working/<slug>.md` | PUT |
| Task-scoped context / focus | `_agent/context/current-context.md` | PATCH / PUT |
| Working-session log | `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
| Long-running project state | `projects/<lifecycle>/<slug>.md` (lifecycle: `incubating``active``on-hold`/`archived`; folder and `status:` MUST agree) | 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` (mirror only into an existing project note's `## Key Decisions`; otherwise skip) | 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 build work) | `_agent/skills/active/<slug>.md` (→ `archived/` when retired) | PUT |
| Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` | POST / PATCH |
| Journal rollup | `journal/{weekly/YYYY-Www,monthly/YYYY-MM,quarterly/YYYY-Qn,annual/YYYY}.md` (weekly = opt-in on first session of a new ISO week; monthly = offered with Vault Health; quarterly/annual = manual) | PUT |
| Vault-health audit (agent self-maintenance) | `_agent/health/YYYY-MM-vault-health.md` (monthly; NOT a journal entry) | PUT |
| Session-end orientation pointer | `_agent/heartbeat/last-session.md` (one line, overwritten each session end) | PUT |
| Bootstrap marker (plugin-owned) | `_agent/chorus-vault.md` (`schema_version`, bootstrap date) — the "is this vault set up?" probe | GET / PUT |
**Slug rules:** kebab-case, ASCII, ~40 chars max. Every file carries canonical frontmatter (see `vault-layout.md`).
@@ -0,0 +1,142 @@
# Bootstrap & Repair
The **plugin is the single source of truth** for CHORUS's structure. Everything needed to stand up a vault ships in this skill under `scaffold/` — there is no dependency on any in-vault control doc and no external/local re-seed path. This makes the vault portable: point the REST API at any empty Obsidian vault, run this procedure, and it becomes a working CHORUS vault.
The vault holds **data only**. It carries no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md`. The "is this vault set up?" signal is a small marker file, `_agent/chorus-vault.md`.
## Quick path — run the scripts
Bootstrap, repair, and migration are deterministic scripts; prefer them over running the curl steps by hand. They resolve the scaffold relative to their own location, so they work regardless of the caller's CWD:
```bash
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts"
python3 "$SCRIPTS/bootstrap.py" --dry-run # preview what would be seeded
python3 "$SCRIPTS/bootstrap.py" # idempotent, additive — fills only what is missing (also the repair path)
python3 "$SCRIPTS/migrate.py" # plan a schema migration (dry-run)
python3 "$SCRIPTS/migrate.py" --apply # perform the migration (moves/deletes, after review)
# Windows: use `python` or `py -3` if `python3` is not on PATH.
```
`bootstrap.py` writes through `chorus.py`, so every step is status-checked and the marker is written last. The scripts are pure Python (only an interpreter required — no bash, no platform-specific `date`), so they run identically on Windows, macOS, and Linux. The manual steps below document what the script does (and serve as a fallback if the script can't run).
```
AUTH="Authorization: Bearer $CHORUS_KEY" # export CHORUS_KEY first; chorus.py resolves it automatically via chorus_config
BASE="$CHORUS_BASE" # the endpoint from ~/.claude/chorus-memory/config.json (override: CHORUS_BASE)
```
---
## Probe — is the vault bootstrapped?
At session start, GET the marker:
```bash
curl -s -o /dev/null -w "%{http_code}" -H "$AUTH" "$BASE/vault/_agent/chorus-vault.md"
```
- **200** → bootstrapped. Read the marker's `schema_version`; if it is **less than** the plugin's current schema (4), run a migration pass (see *Migrations* below), otherwise proceed straight to the loading procedure in `SKILL.md`.
- **404** → empty/unconfigured vault. Run **Fresh bootstrap** below. (If you expected an existing vault, confirm with the operator once that the REST API is pointed at the right vault before seeding.)
---
## Fresh bootstrap (empty vault)
Idempotent and additive. **Never overwrite an existing file** — GET-probe each path and skip any that already returns `200`. The marker is written **last**, so the vault is only flagged "set up" after every piece is in place.
Throughout, `{{DATE}}` means today's date (`YYYY-MM-DD`), resolved from the conversation's `currentDate`. Substitute it before PUTting any scaffold file or anchor seed.
### 1. Folder tree
The REST API auto-creates parent directories on PUT, so creating the tree = seeding a file into each leaf. Obsidian and git both ignore empty dirs, so drop a one-line `README.md` into any leaf that wouldn't otherwise receive a file. Required tree (leaves marked `→ README`):
```
inbox/captures inbox/imports inbox/processing-log
journal/daily journal/weekly journal/monthly journal/quarterly journal/annual journal/templates
projects/active projects/incubating projects/on-hold projects/archived
areas/business areas/personal areas/learning areas/systems
resources/concepts resources/references resources/people resources/companies resources/meetings
decisions/by-date
_agent/context _agent/memory/working _agent/memory/episodic _agent/memory/semantic
_agent/sessions _agent/health _agent/templates _agent/heartbeat
_agent/skills/active _agent/skills/archived
```
> `decisions/by-project/` is intentionally **not** created — it is retired. A project-relevant decision is mirrored as a `[[wikilink]]` under that project's `## Key Decisions` heading instead.
>
> `reviews/` is **not** created — it is retired. Journal rollups (weekly/monthly/quarterly/annual) live under `journal/`; the monthly vault-health audit lives under `_agent/health/`.
A leaf README is just a one-liner, e.g.:
```bash
printf '# %s\n\nMemory vault folder. See the chorus-memory plugin for conventions.\n' "captures" \
| curl -s -X PUT -H "$AUTH" -H "Content-Type: text/markdown" --data-binary @- \
"$BASE/vault/inbox/captures/README.md"
```
### 2. Templates
PUT every file under this skill's `scaffold/templates/` to its mirrored vault path. The tree mirrors 1:1:
| Scaffold file | Vault path |
|---|---|
| `scaffold/templates/_agent/templates/session-log-template.md` | `_agent/templates/session-log-template.md` |
| `scaffold/templates/_agent/templates/context-bundle-template.md` | `_agent/templates/context-bundle-template.md` |
| `scaffold/templates/_agent/templates/working-memory-template.md` | `_agent/templates/working-memory-template.md` |
| `scaffold/templates/_agent/templates/semantic-memory-template.md` | `_agent/templates/semantic-memory-template.md` |
| `scaffold/templates/journal/templates/daily-note-template.md` | `journal/templates/daily-note-template.md` |
| `scaffold/templates/journal/templates/weekly-review-template.md` | `journal/templates/weekly-review-template.md` |
| `scaffold/templates/projects/project-template.md` | `projects/project-template.md` |
| `scaffold/templates/decisions/decision-template.md` | `decisions/decision-template.md` |
Templates keep their Obsidian Templater tokens (`{{date:YYYY-MM-DD}}` etc.) verbatim — those are resolved by Templater / by the skill's daily-note routine, not at seed time.
Resolve scaffold paths against the skill directory — **never a bare relative `@scaffold/...`**, which assumes the caller's CWD is the skill dir and silently sends an empty body otherwise:
```bash
SCAFFOLD="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scaffold"
curl -s -X PUT -H "$AUTH" -H "Content-Type: text/markdown" \
--data-binary @"$SCAFFOLD/templates/journal/templates/daily-note-template.md" \
"$BASE/vault/journal/templates/daily-note-template.md"
```
### 3. Anchor seeds (only if absent — no fabricated facts)
Substitute `{{DATE}}` → today, then PUT each:
| Scaffold file | Vault path |
|---|---|
| `scaffold/anchors/operator-preferences.seed.md` | `_agent/memory/semantic/operator-preferences.md` |
| `scaffold/anchors/current-context.seed.md` | `_agent/context/current-context.md` |
| `scaffold/anchors/inbox.seed.md` | `inbox/captures/inbox.md` |
The operator-preferences seed is deliberately empty — **do not invent preferences.** Real facts accrue through use.
### 4. Vault README (human signpost)
Substitute `{{DATE}}` if present, then PUT `scaffold/README.vault.md``/vault/README.md`. This is the only human-facing control doc in the vault and is **not** read by the agent for routing.
### 5. Marker (write last)
Substitute `{{DATE}}`, then PUT `scaffold/chorus-vault.md``/vault/_agent/chorus-vault.md`. Once this returns `200`, the vault is bootstrapped.
### 6. First-run trace
Create today's daily note from `journal/templates/daily-note-template.md` (resolve the `{{date:…}}` tokens to today), append a one-line `## Agent Log` entry noting the bootstrap, and write a session log in `_agent/sessions/YYYY-MM-DD-HHMM-bootstrap.md`. Tell the operator briefly what was created.
---
## Repair (existing vault, something missing)
Run the same steps 15, but GET-probe each path first and **only create what is missing**. Never overwrite. If a file exists but looks malformed, flag it in the session log rather than replacing it. Repair is safe to run any time and is the right response if the loading procedure hits an unexpected `404` on a structural path.
---
## Migrations (`schema_version` mismatch)
`scripts/migrate.py` automates this: it reads the marker's `schema_version`, applies each intervening migration (idempotent, additive; destructive steps gated behind `--apply` and printed first), and stamps the marker at the end. Run it dry-run, review the plan, then `--apply`. The steps below are what it encodes — and the manual fallback.
When the marker's `schema_version` is older than the plugin's, apply the migration steps for each intervening version, then PATCH the marker's `schema_version` frontmatter to the new value.
- **0 → 1** (control-docs-in-plugin): the vault previously carried root control docs (`CLAUDE.md`, `BOOTSTRAP.md`, `STRUCTURE.md`, `index.md`). Back them up outside the vault, DELETE them, PUT the thin `scaffold/README.vault.md` over the old verbose `README.md`, write the `_agent/chorus-vault.md` marker, and scrub now-dangling `[[CLAUDE]]`/`[[BOOTSTRAP]]`/`[[STRUCTURE]]`/`[[index]]` links from the `## Related` sections of `operator-preferences.md` and `current-context.md` (leave historical session logs alone). Confirm with the operator before deleting.
- **1 → 2** (reviews-folded-into-journal): the `reviews/` tree is retired. (a) For each note under `reviews/weekly/` and `reviews/monthly/`, MOVE it into `journal/weekly/` (rename `YYYY-Www-review.md``YYYY-Www.md`) and `journal/monthly/` respectively, preserving the earliest `created:`. (b) Move any `reviews/monthly/YYYY-MM-vault-health.md` to `_agent/health/`. (c) Move `reviews/quarterly|annual/` artifacts to `journal/quarterly|annual/`. (d) Update inbound `[[reviews/...]]` wikilinks in `## Related` sections to the new paths. (e) DELETE the now-empty `reviews/` tree. Confirm with the operator before deleting; leave historical session logs alone. *(This step covers any vault bootstrapped under schema 1.)*
- **2 → 3** (entity index + cross-linking): adds the `_agent/index/` folder for the machine-maintained entity registry. `migrate.py` creates the folder; the **data back-fill is `sweep.py`** — after migrating, run `python3 scripts/sweep.py --apply` to build `_agent/index/entities.json` from the notes already in the vault and to symmetrize existing `## Related` cross-links (it adds only the missing reciprocal direction, never invents links). `sweep.py` is idempotent and dry-run by default; re-run it any time `/chorus-health` reports index drift.
@@ -0,0 +1,45 @@
# CHORUS — Operating Contract
The durable, client-independent contract for any agent operating against the CHORUS vault. These principles and safety rules formerly lived in the vault's `CLAUDE.md`; they now live in the plugin so they survive regardless of what is (or isn't) in the vault. Day-to-day *procedure* — loading order, search-first, triage, scope switching, PATCH/append rules — is owned by `SKILL.md`. This file holds the things that don't change between sessions or clients.
## What this agent is
You are an agent operating against an Obsidian vault that functions as a shared, long-term memory substrate for human work, Claude Code / CoWork sessions, and other REST API clients. Your role is to read context, synthesize across notes, produce structured outputs, update memory carefully, and leave durable traces in the vault rather than keeping important state only in the conversation. The vault is the **system of record**, not a scratchpad.
## Core principles
- Treat the vault as the system of record for long-term memory.
- Prefer Markdown, YAML frontmatter, wiki-links, and stable folder conventions over proprietary structures.
- Write notes so both humans and agents can understand them without hidden context.
- Preserve history instead of silently overwriting important decisions, summaries, or inferred preferences.
- Keep agent-managed content (`agent_written: true` + `source_notes`) clearly separated from human-authored content.
- Default to **additive updates, explicit status changes, and traceable summaries.**
- Optimize for portability: the same vault should work across Claude Code, CoWork, and any REST API client.
## Memory model (where things live)
- **Working** → `_agent/memory/working/` — transient, time-boxed.
- **Episodic** → `_agent/memory/episodic/` — what happened, when.
- **Semantic** → `_agent/memory/semantic/` — durable facts, patterns, preferences (`operator-preferences.md`).
- **Context bundles** → `_agent/context/` — task-focused reading lists and active state.
## 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.
- Do not store secrets or API keys inside the vault, and never write the API key into a vault note.
- Default to additive updates and explicit status changes over destructive edits.
## REST/API readiness
Assume clients may operate without filesystem access, through the Obsidian Local REST API. Keep paths predictable, frontmatter parseable, titles stable, and all state stored in notes rather than hidden conversation memory. Structure must stay portable: a fresh, empty vault is brought fully online by the plugin's bootstrap (`references/bootstrap.md`), so nothing essential should depend on files existing in the vault ahead of time.
## Concurrency (shared vault)
The vault is a **shared** substrate — Claude Code, CoWork, and other REST API clients may operate on it concurrently. The REST API offers no transactions, so writers coordinate cooperatively:
- Single-line, overwrite-style files (`_agent/heartbeat/last-session.md`, `current-context.md::Scope`) and append targets (`inbox.md`, `## Agent Log`) assume **one writer at a time**. Two sessions writing at once can clobber or duplicate.
- Before a burst of writes in a session that may overlap another, take the advisory lock (`chorus.py lock <id>``_agent/locks/vault.lock`) and release it at session end. The lock is read-back-confirmed and cooperative with a TTL (stale locks are reclaimable); it is a courtesy, not a hard mutex.
- Idempotent append (read-before-POST, whole-line match, via `chorus.py append`) is the second line of defense against duplicate lines from retries or overlapping sessions.
- Status-check every write. A write that returns `>= 400` did **not** land — surface it rather than assuming success.
@@ -0,0 +1,128 @@
# CHORUS Routing Map
**This document is canonical and complete.** Every write destination in the vault appears here exactly once, with the condition that routes content to it, what lands there, and why it is distinct from its neighbours. The rule for the whole system: **if a path is not in this map, nothing is written to it.** A path that cannot justify its separateness from a neighbour is a merge candidate, not a valid destination.
Views of the same truth: `scripts/routing.json` is the **machine-readable canonical source** (one route per destination, as a regex pattern + trigger + reason); `vault_lint.py` loads it and flags any vault path that matches no route (and any write to a retired path). This prose map is the human-readable authority and must stay in sync with `routing.json``scripts/check_routing.py` enforces that mechanically (every path here matches a route, every route appears here). The `SKILL.md` *Where to Write* table is the quick-reference and `vault-layout.md` is the physical tree. When they disagree, `routing.json` + this map win and the others are fixed to match.
## How to read a row
- **Trigger** — the observable condition that sends content here. If two rows could both fire, the more specific trigger wins.
- **What lands** — the unit of content written.
- **Distinct because** — the one reason this path is not merged into its nearest neighbour. This is the load-bearing column; a row without it is a bug.
- **Method** — the dominant REST verb (see `api-reference.md` for mechanics).
---
## inbox/ — unsorted intake
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `inbox/captures/inbox.md` | "Save this, sort later" — destination genuinely unknown at capture time | One date-prefixed line | The only path whose contract is *deferred routing*; everything else here has a known home | POST |
| `inbox/imports/<slug>.md` | Raw external material dropped in wholesale (export, paste, dump) | The raw artifact, unedited | Holds un-triaged *bulk*, vs `captures` which holds single lines | PUT |
| `inbox/processing-log/YYYY-MM-DD.md` | An inbox item is routed to its real home | One line: `<original> → <destination path>` | Audit trail of moves — never holds memory itself, only the record of routing | POST |
Captures and imports are temporary by contract. Triage drains them into the homes below and logs the move; the original is left until the operator okays deletion.
## journal/ — the one append-only time-series stream
Rollups are coarser-grained journal entries over the same timeline, so they live in the same tree. There is no separate `reviews/` tree.
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `journal/daily/YYYY-MM-DD.md` | First agent activity on a given day | `## Agent Log` line(s) + day notes | Finest grain; the only journal note PATCHed repeatedly within its period | PATCH (auto-create) |
| `journal/weekly/YYYY-Www.md` | First substantive session of a new ISO week — **opt-in**, offer first | Light digest: open `active/` threads, aging inbox, week's scope changes | Coarser than daily, finer than monthly; ISO-week grain | PUT |
| `journal/monthly/YYYY-MM.md` | First substantive session of a new month — offered alongside Vault Health | Month digest, same shape as weekly | Month grain; separate cadence and trigger from weekly | PUT |
| `journal/quarterly/YYYY-Qn.md` | **Manual / on request only** | Quarter-scale narrative review | Strategic grain; never auto-fires | PUT |
| `journal/annual/YYYY.md` | **Manual / on request only** | Year-scale narrative review | Coarsest grain; never auto-fires | PUT |
| `journal/templates/` | Bootstrap only (seeded from plugin masters) | Note templates | Holds templates, not journal content; never a runtime routing target | PUT (seed) |
## projects/ — work with an end state
Lifecycle folders; `status:` frontmatter MUST equal the folder name (the linter checks this).
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `projects/active/<slug>.md` | Work in motion now | Project note (Status PATCHed fresh) | Default state for anything being worked | PUT + PATCH |
| `projects/incubating/<slug>.md` | Idea captured, work not started | Project note | Pre-work; promote to `active/` when work begins | PUT |
| `projects/on-hold/<slug>.md` | Paused but still tracked | Project note | Resumable; distinct from `archived` (which is terminal) | PUT |
| `projects/archived/<slug>.md` | Done, abandoned, or rolled up | Project note | Terminal; kept for history, never deleted | PUT |
**Project vs area:** has an end state → `projects/`. Never "done" → `areas/`.
## areas/ — standing responsibilities, no finish line
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `areas/<domain>/<slug>.md` | Ongoing domain the operator maintains indefinitely (`<domain>`: business/personal/learning/systems) | Area note | No end state — the one thing that disqualifies it from `projects/` | PUT |
## resources/ — reference material about the world
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `resources/people/<name>.md` | A fact about a specific person | Person note (kebab-case slug) | Keyed to a person, not a topic or event | PUT / PATCH |
| `resources/companies/<slug>.md` | A fact about an organization (client, vendor, partner, employer) | Company note (kebab-case slug) | Keyed to an organization, not an individual (`people/`) or an external source (`references/`) | PUT / PATCH |
| `resources/concepts/<slug>.md` | A reusable concept/idea the operator wants on file | Concept note | An idea, vs a `reference` which is an external source | PUT |
| `resources/references/<slug>.md` | An external source/link worth keeping | Reference note | Points outward (a source), vs `concepts` (an idea) | PUT |
| `resources/meetings/YYYY-MM-DD-<slug>.md` | Notes/recap tied to a specific meeting or call | Meeting note; mirror decisions to `decisions/by-date/`, commitments to the project | Event-anchored to a meeting, vs a project's ongoing thread | PUT |
## decisions/ — non-obvious decisions (ADR)
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `decisions/by-date/YYYY-MM-DD-<slug>.md` | A non-obvious decision worth recording | ADR: Context → Decision → Consequences | The chronological system of record for decisions | PUT |
| `decisions/decision-template.md` | Bootstrap seed only | ADR template | Template, not a decision; never a runtime routing target | PUT (seed) |
**Mirror, don't duplicate:** if the decision belongs to an existing `projects/active/` note, PATCH a `[[wikilink]]` to the ADR under that project's `## Key Decisions`. No matching project → skip the mirror; the by-date ADR stands alone.
## _agent/ — the agent's own working substrate
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `_agent/chorus-vault.md` | Bootstrap / schema migration only | Marker: `schema_version`, bootstrap date | Plugin-owned probe; never hand-edited | GET / PUT |
| `_agent/echo-vault.md` | Pre-fork ECHO vault adoption (read-only probe) | Legacy ECHO marker, honored until the rename migration | CHORUS is a fork of ECHO; adopted vaults still carry the old marker | GET |
| `_agent/context/current-context.md` | Active scope changes; task focus shifts | `## Scope`, `## Scope History`, priorities | Single *live* scope pointer, vs episodic which is a *past* record | PATCH / PUT |
| `_agent/memory/semantic/operator-preferences.md` | A preference/pattern about the operator | Append under `## Observations`; promote to `## Fact / Pattern` when stable | The one curated profile; distinct from ad-hoc semantic notes | PATCH |
| `_agent/memory/semantic/<slug>.md` | A durable fact/pattern that isn't an operator-preference | Semantic note | Timeless fact, vs episodic (time-stamped event) and working (transient) | PUT |
| `_agent/memory/episodic/<slug>.md` | A record of *what happened, when* | Episodic note | Anchored to an event in time; not a standing fact | PUT |
| `_agent/memory/working/<slug>.md` | Short-lived state needed only for the current effort | Working note | Explicitly transient/time-boxed; safe to go stale | PUT |
| `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | A substantive session ends (decisions/artifacts/commitments) | Session log (see template) | Per-session record; the unit loading Step 4 scans | PUT |
| `_agent/health/YYYY-MM-vault-health.md` | First substantive session of a month (Vault Health pass) | Health-audit findings | Agent self-maintenance about vault integrity — NOT the operator's work narrative, so not in `journal/` | PUT |
| `_agent/heartbeat/last-session.md` | End of every session, after the session log is written | One line: `<session-log-path> @ <ISO-timestamp>` | O(1) orientation pointer read first at load (Step 4); overwritten, never grows | PUT |
| `_agent/templates/` | Bootstrap only (seeded from plugin masters) | Canonical note templates | Holds templates, not memory; never a runtime routing target | PUT (seed) |
| `_agent/skills/active/<slug>.md` | The operator authors/installs a skill and wants it catalogued | Skill capability entry | Catalogs a *capability*, vs `projects/` which tracks the *build effort* | PUT |
| `_agent/skills/archived/<slug>.md` | A catalogued skill is retired | Skill entry (moved from `active/`) | Terminal state of the skill catalog | PUT |
| `_agent/locks/vault.lock` | Advisory multi-writer lock acquire/release | One line: `<owner> @ <ISO-timestamp>` | Concurrency coordination, not memory; managed only by `chorus.py lock/unlock` | PUT / DELETE |
| `_agent/index/entities.json` | Rebuilt on every `capture` and on `sweep` | Slug→{path, kind, title, aliases, last_seen} registry | Machine-maintained routing/recall index, not a note; the one JSON in the vault | PUT |
---
## Structural signposts (not memory)
Created at bootstrap; never a runtime memory destination, but valid vault paths so the linter must recognise them.
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `<folder>/README.md` | Bootstrap leaf signpost (and the vault-root README) | One-line human note | Human signpost; never read for routing | PUT (seed) |
| `projects/project-template.md` · `decisions/decision-template.md` · `journal/templates/` · `_agent/templates/` | Bootstrap seed only | Note templates | Hold templates, not content | PUT (seed) |
---
## Retired paths — explicitly never written
Listed so they are recognised as dead and never recreated. Any one of these appearing in a live vault is a migration miss (see `bootstrap.md` Migrations).
| Path | Status | Where it went instead |
|------|--------|-----------------------|
| `reviews/` (weekly/monthly/quarterly/annual) | Retired in schema 2 | Journal rollups → `journal/{weekly,monthly,quarterly,annual}/`; vault-health → `_agent/health/` |
| `decisions/by-project/` | Retired in schema 1 | ADR mirrored as a `[[wikilink]]` under the project's `## Key Decisions` |
| `archive/` (top-level) | Never existed | Project archival → `projects/archived/`; skill archival → `_agent/skills/archived/` |
| `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` (in-vault) | Retired in schema 1 | All control logic lives in the plugin (`references/`), not the vault |
## Routing decision tree (the calls that get mis-made)
1. **Destination unknown right now?**`inbox/captures/`. Known? → route directly; never park a known fact in the inbox.
2. **Is it about the operator's work over time?**`journal/` (pick the grain by cadence). **About the vault's mechanical health?**`_agent/health/`. These two look similar monthly but answer different questions.
3. **Does the effort have an end state?**`projects/` (folder = `status:`). **No finish line?**`areas/`.
4. **A fact about the world:** timeless → `semantic/`; a thing that happened → `episodic/`; needed only for now → `working/`. A fact about a *person*`resources/people/`; a fact about an *organization*`resources/companies/`.
5. **A decision:** always `decisions/by-date/`; mirror into a project only if one already exists.
6. **A capability (skill/plugin):** `_agent/skills/` catalogs the capability; `projects/` tracks building it. Both can exist for the same skill.
@@ -0,0 +1,110 @@
# Session Log Template
Session logs go in: `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md`
**Filename format is canonical and not optional.** The four-digit local-time HHMM component is what makes session filenames lex-sort in true chronological order — the loading procedure depends on it. Before PUT-ing a new session log, validate the filename matches `^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9-]+\.md$`. Legacy session logs without HHMM exist in the vault; do not edit their names, but every new write must use the full form.
The slug describes what the session was about in 25 words, kebab-case.
Examples: `2026-06-05-1430-chorus-plugin-build.md`, `2026-05-14-0900-q1-review-prep.md`.
Keep logs focused. Capture the goal, what was read/done, decisions, outputs, open threads, and the next step. This matches the vault's `_agent/templates/session-log-template.md`.
---
## Template
```markdown
---
type: session-log
status: complete
created: 2026-06-05T14:30
updated: 2026-06-05T14:30
tags: [agent, session]
agent_written: true
source_notes: []
session_date: 2026-06-05
client: claude-code
---
# Session Log
## Goal
One line — what this session set out to do.
## Notes Read
- [[note]] — why it was relevant
(omit if none)
## Actions Taken
Brief narrative or bullets of what was done.
## Decisions Made
- Decision — why
(omit section if none)
## Outputs Created
- `path/or/filename` — what it is
(omit section if none)
## Open Threads
- [ ] unresolved question or next step
(omit section if none)
## Suggested Next Step
One sentence: what to do first next time on this topic.
## Related
- [[wikilinks go here, in the body — never in frontmatter]]
```
---
## Example
```markdown
---
type: session-log
status: complete
created: 2026-06-05T14:30
updated: 2026-06-05T14:30
tags: [agent, session, plugin]
agent_written: true
source_notes: ["resources/references/obsidian-local-rest-api.md"]
session_date: 2026-06-05
client: claude-code
---
# Session Log
## Goal
Build and package the chorus-memory CoWork plugin against the live vault.
## Notes Read
- [[BOOTSTRAP]], [[STRUCTURE]], [[_agent/memory/semantic/operator-preferences]]
## Actions Taken
Verified the REST API end-to-end, confirmed the scaffold copied into the live vault,
created missing empty folders, and built the plugin (SKILL + 4 reference files).
## Decisions Made
- Vault addressed at root — CHORUS is a dedicated vault.
- Key hardcoded in the plugin (not in the vault) — personal plugin, per the reference pattern.
## Outputs Created
- `chorus-memory.plugin` — installable CoWork plugin
## Open Threads
- [ ] Validate Claude Code direct filesystem access to the vault host.
## Suggested Next Step
Install the plugin and run a live load/write through it to confirm the skill triggers.
## Related
- [[projects/active/vault-foundation]]
- [[resources/references/obsidian-local-rest-api]]
```
After writing the log, append a one-line entry to the daily note's **Agent Log** section.
> **Reminder:** wiki links go in the body (e.g. this `## Related` section), never in YAML
> frontmatter. `source_notes` in frontmatter holds plain relative path strings, not `[[links]]`.
@@ -0,0 +1,187 @@
# Vault Layout & Frontmatter Conventions
**This document is canonical.** The CHORUS vault holds data only — there are no `CLAUDE.md` / `STRUCTURE.md` / `BOOTSTRAP.md` / `index.md` control docs in it. Layout, taxonomy, and frontmatter conventions live here in the plugin; the bootstrap procedure (`references/bootstrap.md`) builds the tree below into any empty vault.
## Folder Map (root-addressed)
```
/vault/
├── README.md ← thin human signpost (NOT read for routing)
├── inbox/
│ ├── captures/ ← quick captures (inbox.md), date-prefixed lines
│ ├── imports/ ← raw imported material
│ └── processing-log/
├── journal/ ← one append-only time-series stream; rollups are coarser journal entries, NOT a separate reviews/ tree
│ ├── daily/ ← YYYY-MM-DD.md (has an "Agent Log" section)
│ ├── weekly/ ← YYYY-Www.md (ISO week, opt-in rollup)
│ ├── monthly/ ← YYYY-MM.md (monthly rollup)
│ ├── quarterly/ ← YYYY-Qn.md (manual / on request)
│ ├── annual/ ← YYYY.md (manual / on request)
│ └── templates/
├── projects/ ← lifecycle: incubating → active → on-hold/archived
│ ├── active/ ← current work (status: active)
│ ├── incubating/ ← idea captured, not yet started (status: incubating)
│ ├── on-hold/ ← paused but kept (status: on-hold)
│ ├── archived/ ← done / abandoned (status: archived)
│ └── project-template.md
├── areas/ ← business / personal / learning / systems
├── resources/
│ ├── companies/ ← <slug>.md — organizations (clients, vendors, partners, employers)
│ ├── concepts/ references/ meetings/
│ └── people/ ← <name>.md
├── decisions/
│ ├── by-date/ ← YYYY-MM-DD-<slug>.md (ADR-style) — the canonical home
│ └── decision-template.md
│ (decisions/by-project/ is retired and not created —
│ mirror an ADR into a project's `## Key Decisions` heading instead)
│ (reviews/ is retired — journal rollups live under journal/; vault-health audits under _agent/health/)
└── _agent/
├── chorus-vault.md ← bootstrap marker: schema_version + bootstrap date (plugin-owned; the "is this vault set up?" probe)
├── context/ ← current-context.md and task bundles
├── memory/
│ ├── working/ ← transient, time-boxed
│ ├── episodic/ ← what happened, when
│ └── semantic/ ← durable facts/patterns (operator-preferences.md)
├── sessions/ ← YYYY-MM-DD-HHMM-<slug>.md
├── health/ ← YYYY-MM-vault-health.md (monthly self-maintenance audit; NOT a journal entry)
├── templates/ ← canonical note templates
├── skills/ ← active / archived
├── heartbeat/ ← single-line pointers (e.g. last-session.md → most-recent session log path)
├── index/ ← entities.json — machine-maintained slug→{path,kind,title,aliases} registry
└── locks/ ← vault.lock — cooperative advisory multi-writer lock
```
**Entity index:** `_agent/index/entities.json` is a machine-maintained registry mapping each entity slug to its `{path, kind, title, aliases, last_seen}`. It makes routing/resolve an O(1), alias-aware lookup and supplies the name→path map for cross-linking and recall. `resolve` matches slug/title/alias exactly; when nothing matches exactly it falls back to **fuzzy candidates** — entities sharing a distinctive token with the mention — so a shortened name ("chorus memory" → the project `chorus`) surfaces the existing note instead of spawning a duplicate. Aliases are auto-derived from titles, learned from mentions on update, and re-folded from note frontmatter on `sweep`. It is rebuilt automatically by `chorus.py capture` and by `sweep.py`; do not hand-edit. The only JSON file in the vault.
**Heartbeat:** `_agent/heartbeat/last-session.md` is a one-line pointer (`<session-log-path> @ <ISO-timestamp>`) the **session-logging procedure writes (PUT, overwrite) at session end** and the **loading procedure reads first (Step 4)** as an O(1) shortcut to the latest session log. It is a hint, not a source of truth — fall back to the `sessions/` directory listing if it's missing or stale. Because it's PUT-overwritten, it never grows or duplicates.
**Slug rules:** kebab-case, ASCII only, truncate to ~40 chars.
---
## Canonical Frontmatter
Every note starts with this block. Fill what applies; leave the rest empty rather than guessing — **except `status:` and `tags:` on entity notes** (person/company/concept/reference/project/area/skill, plus `tags` on meeting/decision), which must be populated: `capture` stamps a kind-appropriate default `status` and seeds `tags` with the note's kind, `/chorus-health` flags missing/empty ones (`incomplete-frontmatter`), and `sweep.py --apply` backfills older notes. The per-kind requirement map is `KIND_REQUIRED_FM` / `KIND_STATUS` in `scripts/chorus_index.py`.
```yaml
---
type: # see Note Types below
status: # active | draft | done | archived | complete — REQUIRED on entity notes (kind default stamped by capture)
created: # YYYY-MM-DD (or YYYY-MM-DDTHH:mm for sessions)
updated: # YYYY-MM-DD
tags: [] # REQUIRED (non-empty) on entity notes — capture seeds the kind (e.g. [company]); enrich via --tags
aliases: [] # optional — other names this entity is called (Obsidian-native). Folded
# into the entity index so a shortened/expanded name resolves to this note.
agent_written: false
source_notes: [] # plain relative paths as strings — NEVER [[wikilinks]]
---
```
`aliases:` is the **durable, Obsidian-native home for alternate names** of an entity (e.g. the project `chorus` is also "chorus-memory" / "chorus plugin"). `capture` auto-derives the obvious case/punctuation variants of the title and writes them here; `sweep` folds frontmatter aliases back into the entity index, so they survive an index rebuild. List plain strings, never `[[wikilinks]]`.
`agent_written: true` + a populated `source_notes` is the key signal separating
agent-managed content from human-authored content. When appending with POST, do
not rewrite frontmatter — the append goes after existing content. To change
`updated:` or `status:`, use PATCH with `Target-Type: frontmatter`.
**Frontmatter field semantics:**
- `created:` is the **earliest known date** the entity was tracked in the vault — *not* "today". When merging notes (e.g. promoting an `on-hold/` project into `active/`), preserve the earliest `created:` from any merged source and only update `updated:`.
- `source_notes` is a **backward link** — the note(s) that triggered or supplied content for this one (e.g. the session log a project update came from). Forward links go in the `## Related` body section, never here.
- `status:` for a project MUST match its folder under `projects/` (`active`, `incubating`, `on-hold`, `archived`). Moving the file and updating `status:` are the same operation.
> **No `[[wikilinks]]` in frontmatter.** YAML parses `[[...]]` as nested lists, so wiki
> links there break and never render as clickable links in reading view. Put all
> cross-references in a **`## Related`** section in the note **body** (bulleted `[[links]]`).
> Frontmatter holds scalar/string metadata only; `source_notes` values are plain relative
> paths, not links.
## Note Types
`daily-note`, `weekly-note`, `monthly-note`, `project`, `project-update`, `area`,
`concept`, `reference`, `person`, `company`, `meeting`, `decision`, `review`, `session-log`,
`working-memory`, `episodic-memory`, `semantic-memory`, `context-bundle`, `skill`,
`draft`, `inbox-item`.
---
## File-Specific Conventions
### operator-preferences.md (`_agent/memory/semantic/`)
The profile analog. Canonical headings:
- `## Operator` — who the vault owner is (one paragraph)
- `## Fact / Pattern`**promoted, deduped rules.** No date prefix. Timeless.
- `## Observations`**timestamped raw observations.** Date-prefixed lines (`- 2026-06-06: ...`). Default landing zone for new evidence.
- `## Evidence` — citations/links supporting the rules
- `## Recommendation or Implication` — how the rules should shape behavior
- `## Review Notes` — confidence / last review date
Append observed facts under `## Observations` by default. Promote to `## Fact / Pattern` (dropping the date) once a pattern stabilizes. "The operator" is the vault owner — refer to them generically and in the third person (the display name is the runtime `owner` config value).
### projects/active/\<slug\>.md
Mirrors `scaffold/templates/projects/project-template.md` — keep this block in sync with that template.
```markdown
---
type: project
status: active
created: {{date:YYYY-MM-DD}}
updated: {{date:YYYY-MM-DD}}
tags: [project]
agent_written: false
source_notes: []
owner:
review_cycle: weekly
---
# Project Name
## Purpose
## Status
One paragraph, kept fresh via PATCH replace (target `Project Name::Status`).
## Goals
## Current Context
## Open Loops
- [ ] unresolved thing
## Key Decisions
- [[YYYY-MM-DD-decision-slug]] — one-line summary (ADR mirror target)
## Related Notes
## Session History
- 2026-06-05: observation or update
## Related
- [[areas/business/business-ops]]
```
### sessions/YYYY-MM-DD-HHMM-\<slug\>.md
See `session-log-template.md`. CHORUS uses an **HHMM time component** in the filename — this is **canonical, not optional**. The four-digit local-time component makes filenames lex-sort in true chronological order, which the loading procedure relies on. Older session logs without HHMM exist; leave them alone, but every new one must use the full `YYYY-MM-DD-HHMM-<slug>.md` form.
### decisions/by-date/YYYY-MM-DD-\<slug\>.md
ADR-style: Context → Decision → Consequences. If the decision belongs to an existing project, PATCH-append the wikilink into that project's `## Key Decisions` heading. Don't use `decisions/by-project/` — it's legacy scaffolding that's intentionally left empty.
### people/\<name\>.md
`type: person`. Use lowercase kebab-case for the slug (e.g. `robert-smith.md`).
### companies/\<slug\>.md
`type: company`. An organization the vault owner works with — client, vendor, partner, or employer (e.g. `acme.md`). Distinct from `people/` (individuals, who *belong to* companies) and `references/` (external sources). Lowercase kebab-case slug.
---
## Cross-References
Use Obsidian wiki links freely: `[[note-name]]` or `[[folder/note]]`. The REST API
doesn't resolve them, but Obsidian does when the operator browses the vault.