---
name: echo-memory
description: Use the ECHO Obsidian vault as Jason's persistent memory across Claude/CoWork sessions. Use whenever Jason asks to remember, save, note, log, or capture anything durable — facts, preferences, decisions, schedule changes, commitments — or asks what Claude knows about him, what was discussed or decided before, to check his notes, load his memory/profile/context, add to his 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 Bryan's goldbrain vault, Obsidian/REST-API development questions, "memory" meaning RAM, timed reminders, email or local-file lookups, generic second-brain advice, or notes Jason routes to a specific other file or app.
---

# ECHO Memory

Use the **ECHO** Obsidian vault as persistent memory. Read context accumulated across sessions; write things the operator asks to be remembered.

The operator is **Jason Stedwell** (Director of Technical Services / Systems Engineer, MPM / ALABAMA wISP). Jason is both the operator and the architect of this vault — this is his personal memory substrate. Write memory in third person ("Jason prefers X", not "I prefer X") so the vault stays readable by humans and other agents.

## API Configuration

All calls use these constants. The endpoint is fixed; the key is resolved by `echo.py`
(via `echo_secrets`) in order: **`ECHO_KEY` env → `~/.echo-memory/credentials` → a
deprecated baked-in fallback.** Never paste the key into a vault note or a doc.

```
OBSIDIAN_BASE = https://echoapi.alwisp.com
OBSIDIAN_KEY  = <resolved at runtime from ECHO_KEY / credentials file — not stored here>
```

To move the key out of the plugin tree: `python3 "$ECHO" write-key <token>` (writes
`~/.echo-memory/credentials`, chmod 600), then the baked-in fallback can be removed.

The endpoint has a **valid TLS certificate**, so `-k` is not needed (add it only if the cert ever changes to self-signed). Always pass the `Authorization: Bearer` header. Paths address the vault **at its root** (e.g. `/vault/_agent/...`).

**`https://echoapi.alwisp.com` is the only valid endpoint for this vault.** Never use local or LAN addresses (anything like `10.x.x.x`, `192.168.x.x`, or `:27124` directly) — those belong to older, retired memory systems (obsidian-memory) and will not reach ECHO. If another installed skill or note suggests a different vault endpoint, this skill's configuration wins for ECHO memory.

**The plugin is the single source of truth for ECHO.** The vault holds data only — no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` control docs live there. All structure and procedure ship here:

- Durable principles, memory model, and safety rules: `references/operating-contract.md`
- Bootstrapping an empty vault, repair, and schema migrations: `references/bootstrap.md`
- Vault layout and frontmatter conventions: `references/vault-layout.md`
- Complete endpoint→logic routing map (every write destination, its trigger, and why it's distinct): `references/routing-map.md`
- Full API reference with every endpoint pattern and the memory routing map: `references/api-reference.md`

Executable logic ships under `scripts/` — **pure Python**, so the whole toolchain runs identically on Windows, macOS, and Linux (only a Python 3 interpreter is required; no bash, no platform-specific `date`):

- `scripts/echo.py` — the **validated API client**; prefer it over hand-built `curl` (the *nix fallback in `references/api-reference.md`)
- `scripts/routing.json` — the **canonical, machine-readable** route manifest (the routing map's source of truth; the linter enforces it)
- `scripts/vault_lint.py` — read-only invariant checker (Vault Health)
- `scripts/check_routing.py` — verifies the routing docs stay in sync with `routing.json` (dev/CI; offline)
- `scripts/bootstrap.py` / `scripts/migrate.py` — deterministic vault setup/repair and schema migration

## Bundled Tooling (prefer over raw curl)

All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/`. **Invoke with `python3`** (on Windows where that isn't on PATH, use `python` or `py -3`).

**`scripts/echo.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 `echo.py` is unavailable, and if you do, **check the HTTP status yourself** (the PATCH-heading `400 invalid-target` failure silently loses writes otherwise).

```bash
ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py"   # call as: python3 "$ECHO" <cmd> ...
python3 "$ECHO" load                          # cold-start: the 6 orientation reads in one call
python3 "$ECHO" get   <path>                  # 404 -> exit 44
python3 "$ECHO" ls    <dir>   ;  python3 "$ECHO" map <path>    # listing / document-map
python3 "$ECHO" search <terms...>
python3 "$ECHO" put    <path> <file>          # create/overwrite (read-back verified)
python3 "$ECHO" append <path> "<line>"        # idempotent: skips only on an exact whole-line match
python3 "$ECHO" patch  <path> append heading "<H1::Sub>" <file>
python3 "$ECHO" fm     <path> updated '"2026-06-19"'   ;  python3 "$ECHO" bump <path>   # frontmatter
python3 "$ECHO" scope  show   ;  python3 "$ECHO" scope set "<text>"
python3 "$ECHO" lock   <session-id>  ;  python3 "$ECHO" unlock <session-id>
```

### High-level memory ops (prefer these — they do the routing for you)

These collapse the multi-step write/recall discipline into one call each, backed by the **entity index** (`_agent/index/entities.json`, a slug→{path,kind,title,aliases} registry). **Default to them** — they are how this skill reduces the input needed to route and cross-link memory:

```bash
python3 "$ECHO" capture "<title>" <bodyfile> --kind <kind> [--aliases a,b] [--source p1,p2] [--status s] [--date YYYY-MM-DD] [--domain business]
python3 "$ECHO" capture "<title>" --inbox            # unknown home -> idempotent inbox line
python3 "$ECHO" resolve "<mention>"                  # mention -> canonical path (or where to create)
python3 "$ECHO" recall  "<query>"                    # search + 1-hop link/source expansion -> connected context
python3 "$ECHO" link    <pathA> <pathB>              # add reciprocal [[Related]] links A<->B
```

- **`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 canonical frontmatter (`type/created/updated/aliases/agent_written/source_notes`), 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, **learns the mention as an alias** when updating an entity under a different name, and **warns if the new note resembles an existing entity** (so a shortened name can't silently spawn a duplicate). `--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.
- **`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. "echo memory" surfaces the project `echo`). 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.
- **`link`** when two existing notes are related but not yet connected; `capture` already auto-links what it detects.

The index is maintained automatically by `capture`; `sweep.py` rebuilds it and back-fills links for an existing vault (see **Vault Maintenance**).

**Bootstrap / migrate** are scripts, not hand-run curl loops: `python3 scripts/bootstrap.py [--dry-run]` (idempotent, probe-before-write, never overwrites) and `python3 scripts/migrate.py [--apply]` (reads the marker's `schema_version` and applies migrations; dry-run by default). See `references/bootstrap.md`.

### Concurrency — the vault is shared, so coordinate writes

ECHO is read/written by multiple clients (Claude Code **and** CoWork sessions). The single-line files (`heartbeat/last-session.md`, `current-context.md::Scope`, `inbox.md`) assume a single writer at a time. Before a burst of writes in a session that may overlap another, take the **advisory lock**, and release it at session end:

```bash
python3 "$ECHO" lock   "cc-<session-id>"     # exit 75 if another session holds a fresh lock, or you lost the race
# ... do the writes ...
python3 "$ECHO" unlock "cc-<session-id>"
```

Use a stable `<session-id>` for both calls (any unique string for this session). The lock is read-back-confirmed (after PUT, `echo.py` re-reads and backs off if another writer won the race) and cooperative (a stale lock past `ECHO_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 Jason another session may be active rather than racing it.

### Slash commands

`/echo-load` (cold-start read), `/echo-save <text>` (route + persist via `capture`), `/echo-recall <query>` (recall a topic's neighbourhood), `/echo-triage` (drain the inbox), `/echo-health` (run the linter), `/echo-sweep` (bring the vault up to current spec). These are explicit entry points to the procedures below.

## Operating Contract & Safety

The vault is the **system of record** for long-term memory, not a scratchpad. Default to **additive updates, explicit status changes, and traceable summaries**. Keep agent-managed content (`agent_written: true` + `source_notes`) separable from human-authored content. Non-negotiable safety rules:

- Do not fabricate facts, relationships, or prior decisions.
- Do not mass-restructure the vault unless explicitly asked.
- Do not delete notes unless deletion is explicitly requested and clearly safe.
- Never store secrets or API keys inside a vault note.

Full contract (principles, agent role, memory model): `references/operating-contract.md`.

## When to Load Memory

Load at the start of any substantive conversation — anything beyond a single quick factual question. The signal: Jason is starting work, planning, asking for help with something that has state, or referencing prior discussions.

### Loading procedure

**Run `python3 "$ECHO" load`** — one call that performs the six orientation reads below and prints each labeled section (404s on today's note / inbox show as absent, not errors; an absent marker is flagged). This replaces issuing six separate GETs by hand. The table documents what `load` reads and why:

| # | GET | Notes |
|---|-----|-------|
| 1 | `/vault/_agent/echo-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/operator-preferences.md` | Jason's profile |
| 3 | `/vault/_agent/context/current-context.md` | Active scope + Scope History |
| 4 | `/vault/_agent/heartbeat/last-session.md` → then `/vault/_agent/sessions/` | **Read the heartbeat first** — a one-line pointer (`<session-log-path> @ <ISO-timestamp>`) written at the end of the previous session. It's an O(1) jump to the latest log, so you can skip or shortcut the full listing. Fall back to the `sessions/` listing only if the pointer is missing or looks 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. |
| 5 | `/vault/journal/daily/YYYY-MM-DD.md` | Today's note; 404 is fine — it's created on first agent activity |
| 6 | `/vault/inbox/captures/inbox.md` | Inbox depth probe — feeds the load-time reconcile below. 404 is fine (empty inbox). |

Do not read every session log — older sessions are reachable via `POST /search/simple/?query=...` when needed.

**Reconcile at load (do this every cold start, after the batch returns).** The batch already fetched everything needed for a cheap self-check — run it before diving into the work so memory maintains itself instead of drifting:

1. **Inbox depth (Inbox Triage).** If `inbox/captures/inbox.md` (GET #6) 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 — Jason 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 "$ECHO" 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 "$ECHO" 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 Jason rather than trusting it.

Keep the reconcile to a single short line to Jason (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.

**If a specific topic/project/person is in play**, pull its connected context in one call:

```bash
python3 "$ECHO" recall "<topic or title>"   # the matching notes AND their one-hop neighbourhood
```

`recall` resolves the term against the entity index, searches, and expands one hop along `## Related` links and `source_notes` — so you get the project note *plus* its linked decisions, people, and prior sessions, not an isolated file. (If you only need the canonical path, `resolve "<title>"` is the O(1) lookup.)

Do NOT narrate this loading. Reading memory is expected behavior.

## Inbox Triage

`inbox/captures/inbox.md` is the catch-all for "save this somewhere — figure out where later." Without triage it grows forever and durable facts get stranded there.

At the start of a substantive session, GET `inbox/captures/inbox.md`. If it contains lines older than ~7 days that haven't been routed elsewhere, surface them once:

> "Three captures from last week are still in the inbox — want to route them now or leave them?"

When routing accepted items, send each to its proper home:

- Preference / pattern → PATCH-append under `operator-preferences.md::Observations`
- Project idea → PUT `projects/incubating/<slug>.md`
- Durable fact → PUT `_agent/memory/semantic/<slug>.md`
- Person fact → PUT/PATCH `resources/people/<name>.md`

Then record the move in `inbox/processing-log/YYYY-MM-DD.md` (one line per item: `- <original line> → <destination path>`). Don't delete the original capture unless Jason explicitly asks — the processing log is the audit trail.

## Project Lifecycle

Projects move through four folders under `projects/`. The folder name and the `status:` frontmatter field MUST agree — they are two views of the same state.

| Folder | `status:` | Meaning |
|--------|-----------|---------|
| `projects/incubating/` | `incubating` | Idea captured; not actively worked. Promote when work starts. |
| `projects/active/`     | `active`     | Current work. The default state for anything in motion. |
| `projects/on-hold/`    | `on-hold`    | Paused but still tracked. Resumable. |
| `projects/archived/`   | `archived`   | Done, abandoned, or rolled into something else. Not deleted. |

**Promotion / transition rule:** move the file to the new folder AND update `status:` in the same change. A file in `projects/active/` with `status: on-hold` is broken state — fix it when you see it.

**Searching:** `POST /search/simple/?query=<slug>` covers all four subfolders in one call. Always search before creating a new note (see the pre-write rule below).

## When to Write Memory

Write when Jason:

- 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 Jason. Every note carries the canonical frontmatter (see below). Agent-generated notes set `agent_written: true`.

**Default write path: `capture`.** For a routed memory (a fact about a person/company/project/concept/etc.), prefer `python3 "$ECHO" capture "<title>" <bodyfile> --kind <kind>` — it does the search-first, path derivation, frontmatter, indexing, auto-linking, and Agent-Log line in one call (see **High-level memory ops**). Drop to the lower-level `put`/`patch`/`append` verbs below only for shapes `capture` doesn't model (a project `## Status` replacement, an ADR mirror, a daily-note edit) or to inspect/repair.

### Before you write — search first (MANDATORY for new notes)

`capture` and `resolve` enforce this automatically via the entity index. If you write a slug-addressed note **by hand**, first **`resolve` the title/slug** (it matches aliases and returns the canonical path or confirms none exists); if `resolve` is unavailable, `search` the whole vault for the slug. Listing a single folder (e.g. `projects/active/`) is NOT sufficient — a note with the same slug may exist in `projects/on-hold/`, `projects/incubating/`, `projects/archived/`, or under a different folder entirely.

```bash
python3 "$ECHO" resolve "<title>"     # preferred: alias-aware, O(1)
python3 "$ECHO" search  <slug>        # fallback when the index isn't trusted
```

If a match is found:
- In a non-active project subfolder (`on-hold/`, `incubating/`, `archived/`): **promote/merge** — PUT the merged content to `projects/active/<slug>.md` with `status: active`, then DELETE the old location. Preserve the earliest `created:` date.
- In the same folder you intended to write: **update in place** (PATCH or merged PUT). Never silently overwrite — fold the existing content in first.
- Elsewhere (e.g. a stale duplicate under `resources/`): tell Jason and ask which should be canonical before writing.

**Search both the slug AND any human title** Jason used (e.g. slug `echo-memory` and title `ECHO plugin`). Slug-only searches miss notes filed under a different naming scheme. Two cheap `POST /search/simple/?query=...` calls beat one expensive cleanup pass later.

Only after the search comes back empty (or you've decided to merge) is it safe to create a new note. This rule prevents the most common duplication bug: a note exists in `on-hold/` but the agent only checked `active/` and created a parallel record.

### Before you append — read first (idempotency)

`echo.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 `inbox/captures/inbox.md`, a daily note's `## Agent Log`, or a `## Fact / Pattern` / `## Observations` / `## Log` heading. (A raw POST is **not** idempotent — if you must use curl, GET first and whole-line-match the entry yourself; see `references/api-reference.md`.) This does **not** apply to PUT or PATCH `replace`, which overwrite by design.

### Append, patch, put — via the client

Write multi-line bodies to a file first (use the Write tool — cross-platform, no heredoc) and pass the path; or pipe via stdin (`-`).

```bash
# additive line (idempotent). Anchor the date on the conversation's currentDate.
python3 "$ECHO" append inbox/captures/inbox.md "- <currentDate>: <entry>"

# targeted heading append/replace. The heading Target is the FULL `::`-delimited path
# from the H1 — a bare subheading returns 400 invalid-target and the write is LOST.
# If unsure of the exact path, GET the document map first and copy the heading verbatim:
python3 "$ECHO" map   _agent/memory/semantic/operator-preferences.md
python3 "$ECHO" patch _agent/memory/semantic/operator-preferences.md \
  append heading "Operator Preferences::Fact / Pattern" <bodyfile>
# `replace` (instead of `append`) overwrites a section entirely (e.g. a project's "Name::Status").

# create/overwrite a whole file (read-back verified; intermediate dirs auto-created)
python3 "$ECHO" put projects/active/<slug>.md <bodyfile>

# frontmatter freshness after a SUBSTANTIVE change (status/decision/scope) — NOT routine log appends
ECHO_TODAY=<currentDate> python3 "$ECHO" bump projects/active/<slug>.md   # sets updated: to today

# search (run before creating any slug-addressed note — see the search-first rule above)
python3 "$ECHO" 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`.

## Scope Switching (`current-context.md`)

`_agent/context/current-context.md` tracks a single active scope. Jason routinely shifts scope within a day (echo plugin → MPM brand → WISP docs), so this is high-churn — switch deliberately, every time the work changes topic.

**Preferred — one command:**

```bash
python3 "$ECHO" scope set "<new scope summary>"
```

`scope set` does the whole switch atomically and correctly: it archives the **prior** scope to `## Scope History` (dated, truncated), replaces `## Scope` with the new text, and stamps the `scope_updated:` frontmatter timestamp. That timestamp is the **freshness signal** — it's what `echo.py scope show` and the `vault_lint.py` drift check read to tell whether the recorded scope still reflects current work. Always switch through `scope set` so `scope_updated` stays honest; a hand-edited `## Scope` that skips the stamp reintroduces silent drift.

**Manual fallback** (only if `echo.py` is unavailable): PATCH `prepend` the prior scope to `## Scope History`, PATCH `replace` `## Scope`, then PATCH the frontmatter `scope_updated:` (and `updated:`) to today (see `references/api-reference.md` for the raw curl). Note `scope_updated` must already exist in frontmatter — a `PATCH replace` on a missing field returns `400 invalid-target`; run `bootstrap.py` to add it.

This keeps a rolling trail of recent scopes in one file instead of spawning separate stash notes. Trim Scope History to the last ~10 entries when it grows past that.

**Drift backstop:** `vault_lint.py` flags when ≥ `SCOPE_STALE_SESSIONS` (default 3) session logs are dated after `scope_updated` — i.e. work happened without a scope switch. It's advisory (surfaced in Vault Health / `/echo-health`), the mechanical safety net under the load-time judgment above.

## Journal Rollups (the journal is one continuum)

The journal is a single append-only chronological stream. Rollups are just coarser-grained journal entries over the same timeline, so they **all live under `journal/`** — there is no separate `reviews/` tree. One place to read the whole time-series story, daily through annual.

| Cadence | Path | Trigger | Type |
|---------|------|---------|------|
| Daily | `journal/daily/YYYY-MM-DD.md` | first agent activity that day | `daily-note` |
| Weekly | `journal/weekly/YYYY-Www.md` (e.g. `2026-W24.md`) | first substantive session of a new ISO week — **opt-in**, offer first | `weekly-note` |
| Monthly | `journal/monthly/YYYY-MM.md` | first substantive session of a new calendar month — offer alongside the Vault Health pass | `monthly-note` |
| Quarterly | `journal/quarterly/YYYY-Qn.md` | **manual / on request only** | `review` |
| Annual | `journal/annual/YYYY.md` | **manual / on request only** | `review` |

All filenames lex-sort chronologically. Derive the ISO week (`YYYY-Www`) and month (`YYYY-MM`) from the conversation's `currentDate` and check whether that period's note already exists before offering a rollup.

A weekly/monthly rollup is a **light digest** — open threads across `projects/active/`, inbox items aging past ~7 days, and the period's `## Scope History` changes from `current-context.md`. It is *not* a vault-health audit (that's an agent-maintenance artifact — see below).

## Vault Health (monthly)

On the first substantive session of a calendar month, run a quick health pass and write findings to `_agent/health/YYYY-MM-vault-health.md`. This is **agent self-maintenance, not a journal entry** — it lives under `_agent/` because it's about the vault's mechanical integrity, not Jason's work narrative. Don't auto-fix without asking.

Run the bundled linter first — it mechanically checks the invariants below so you don't eyeball them. **Pass `ECHO_TODAY` = the conversation's `currentDate`** so stale/aging math uses the same clock you write with (not the runner's machine date):

```bash
ECHO_TODAY=<currentDate> python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/vault_lint.py"
```

Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Checks (the linter asserts each and prints violations):

1. **Stale active projects** — for each note in `projects/active/`, check `updated:` >30 days. Likely belongs in `on-hold/`.
2. **Unprocessed inbox** — GET `inbox/captures/inbox.md`. List items older than 14 days that never moved through the triage protocol.
3. **Duplicate slugs across lifecycle folders** — any slug appearing in more than one of `active/`, `incubating/`, `on-hold/`, `archived/` is broken state.
4. **Folder ↔ `status:` mismatch** — any `projects/<lifecycle>/` note whose `status:` frontmatter disagrees with its folder.
5. **Wikilinks in frontmatter** — any `[[...]]` inside a YAML frontmatter block (breaks Obsidian reading view), swept across all folders.
6. **Duplicate `## Agent Log` headings** — any daily note with more than one.
7. **Unknown / retired paths** — any vault file that matches no route in `scripts/routing.json` (or sits at a retired path).
8. **Frontmatter integrity** — missing required fields, `updated` < `created`, future `updated`, and `[[wikilinks]]` leaking into `source_notes`.
9. **Graph health** — **broken wikilinks** (`[[X]]` resolving to no note), **orphans** (an entity note with no inbound links and an empty `## Related`), and **index drift** (entity-index entries whose file is gone, or indexable notes missing from `entities.json` — fix with `sweep.py`).

## 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, (3) **symmetrizes cross-links** (for every `## Related` A→B, adds the missing B→A; it never invents new links from body text), and (4) stamps the marker `schema_version` to current. Dry-run by default:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py"            # plan (read-only)
python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" --apply    # write
```

Run it after `migrate.py` (which handles structural/schema changes) — or any time `/echo-health` reports index drift. It is idempotent; re-running is safe.

The pass is cheap and pays for itself by catching drift before it requires a reorg. Write the findings as a digest; act on them only with Jason's go-ahead.

**Performance.** Full-vault scripts (`sweep.py`, `vault_lint.py`, recall rebuild) are connection-pooled (keep-alive) and read the whole vault concurrently via `echo.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 `ECHO_WORKERS` (default 8, bulk-read concurrency) and `ECHO_TIMEOUT` (default 30s, per-request). For a vault large enough that even the concurrent pass nears the tool timeout, run the script with the harness's background-execution option rather than blocking the turn.

## Daily Note — Agent Log

After substantive activity, write a one-line entry to today's daily note's `## Agent Log` heading. The daily-note **template** (`journal/templates/daily-note-template.md`) defines this heading, but ad-hoc daily notes created without the template don't have it — so PATCH can fail with `invalid-target` if the note exists but lacks the heading.

**Procedure (resilient)** — all dates are the conversation's `currentDate`, not the machine clock:

1. `python3 "$ECHO" get journal/daily/<currentDate>.md`.
2. **404** — fetch `journal/templates/daily-note-template.md`, substitute its `{{date:YYYY-MM-DD}}` tokens to `<currentDate>`, write the result to a file (Write tool), and `put` it to the daily path; then go to step 4.
3. **200 but no `## Agent Log`** — detect by matching an anchored `^## Agent Log` line in the **raw markdown** you just fetched. Do **not** check the document map, whose headings are full `::`-delimited paths (e.g. `2026-06-07::Agent Log`), so a bare `Agent Log` never matches and you'd append a duplicate heading. If missing, `post` a body of exactly `\n\n## Agent Log\n` to add the heading.
4. PATCH-append the entry under `<currentDate>::Agent Log` (the daily note's H1 is the date, so that is the full target path):

```bash
python3 "$ECHO" patch journal/daily/<currentDate>.md append heading "<currentDate>::Agent Log" <bodyfile>
```

## Where to Write

| Situation | File / path | Method |
|-----------|-------------|--------|
| Unsure where it goes / quick capture | `inbox/captures/inbox.md` (date-prefixed line) | POST |
| Operator preference or durable fact | `_agent/memory/semantic/operator-preferences.md` (append under the right heading) | PATCH |
| Other durable facts / patterns | `_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/context/current-context.md` | PATCH / PUT |
| Working session ended with substance | `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
| Long-running project state | `projects/<lifecycle>/<slug>.md` (see Project Lifecycle) | PUT + PATCH |
| Ongoing area of responsibility (standing domain, no end state) | `areas/<domain>/<slug>.md` (domain: `business`/`personal`/`learning`/`systems`) | PUT |
| Non-obvious decision (ADR) | `decisions/by-date/YYYY-MM-DD-<slug>.md` (see mirror note below) | PUT |
| Person context | `resources/people/<name>.md` | PUT / PATCH |
| Company / organization context | `resources/companies/<slug>.md` | PUT / PATCH |
| Concept / reference note | `resources/concepts/` or `resources/references/` | PUT |
| Meeting notes / call recap | `resources/meetings/YYYY-MM-DD-<slug>.md` | PUT |
| Skill / plugin capability entry (catalog, not the build work) | `_agent/skills/active/<slug>.md` (→ `_agent/skills/archived/` when retired) | PUT |
| Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` — see **Daily Note — Agent Log** above | PATCH (with auto-create) |
| Weekly / monthly rollup | `journal/weekly/YYYY-Www.md` · `journal/monthly/YYYY-MM.md` — see **Journal Rollups** | PUT |
| Quarterly / annual review | `journal/quarterly/YYYY-Qn.md` · `journal/annual/YYYY.md` (manual / on request) | PUT |
| Vault-health audit (agent self-maintenance) | `_agent/health/YYYY-MM-vault-health.md` — see **Vault Health** | PUT |
| Session-end orientation pointer | `_agent/heartbeat/last-session.md` (one line: `<session-log-path> @ <ISO-timestamp>`) | PUT |

> **The complete, audited routing map lives in `references/routing-map.md`** — every write destination with its trigger, what lands there, and why it's distinct from its neighbors. This table is the quick-reference; the map is the authority. If a path isn't in the map, it shouldn't be written to.

**Decision mirrors:** if the decision belongs to an existing note in `projects/active/`, add a `[[wikilink]]` to the ADR under that project's `## Key Decisions` heading (PATCH). If no matching project note exists, skip the mirror — the by-date ADR is sufficient; do not invent topical mirror files in `decisions/by-project/`.

**Routing triggers — areas / meetings / skills:**

- **Area** (`areas/`): a standing responsibility Jason maintains with no finish line. Decision rule: has an end state → `projects/`; never "done" → `areas/`. Subfolder is the domain (`business`/`personal`/`learning`/`systems`); create it on first write. `type: area`.
- **Meeting** (`resources/meetings/`): notes, recaps, or action items tied to a specific meeting or call. Mirror decisions to `decisions/by-date/` and commitments to the relevant project. `type: meeting`.
- **Skill** (`_agent/skills/`): Jason authors, installs, or retires a skill/plugin and wants it tracked as a capability — distinct from `projects/`, which tracks the build effort. Active entries in `active/`, retired ones in `archived/`. `type: skill`.

Never delete files unless Jason 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/YYYY-MM-DD-HHMM-<slug>.md`.** Always include the four-digit local-time HHMM component — it makes filenames lexically sort in true chronological order, which is what Step 3 of loading relies on. Older session logs without the HHMM part exist; leave them alone, but every new one must use the full form.

Write the body (see `references/session-log-template.md`) to a file with the Write tool, then:

```bash
python3 "$ECHO" put _agent/sessions/<currentDate>-HHMM-<slug>.md <bodyfile>
```

Then add a one-line entry to today's daily note via the **Daily Note — Agent Log** procedure above.

Finally, update the heartbeat pointer so the next session can orient in one read (this closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read). It is a single line, `<session-log-path> @ <ISO-timestamp>`; write it to a file and PUT it:

```bash
python3 "$ECHO" put _agent/heartbeat/last-session.md <bodyfile>
```

`last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate.

## Vault Unreachable

If the API returns a connection error, timeout, or `502`, tell Jason once that the memory vault is unreachable (a `502` usually means Obsidian/the REST plugin is not running on the backend), then proceed without memory. Do not retry repeatedly.

## Style Rules

- Write in third person about Jason: "Jason prefers X", not "I prefer X".
- Jason is both operator and architect here — unlike goldbrain (Bryan's vault, which Jason architected). Do not cross-write: Bryan's preferences belong in goldbrain, Jason's belong 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).
- **`created:` is the earliest known date the entity was tracked anywhere in the vault, not "today".** When merging notes (e.g. promoting `on-hold/` → `active/`), preserve the earliest `created:` and only bump `updated:`.
- **`source_notes` lists the note(s) that *triggered* or *supplied content for* this one** — e.g. the session log that produced a project update, or the daily note where a captured fact originated. It is a *backward* link to inputs. Forward links (this note → other notes it references) belong in the `## Related` section in the note body, never in frontmatter.
- **Never put `[[wikilinks]]` in frontmatter** — YAML parses them as nested lists and the links break in Obsidian's reading view. Put all cross-references in a `## Related` section in the note **body** as a bulleted list of `[[links]]`.
- Use Obsidian wiki links (`[[note name]]`) freely in the note **body** for cross-references.
- Keep entries short and focused. Fewer, sharper entries beat many noisy ones — Jason explicitly prefers concision.
- About to write something large or sensitive? Show Jason the content first and confirm.

## operator-preferences.md — Rules vs Observations

`_agent/memory/semantic/operator-preferences.md` separates two kinds of content:

- `## Fact / Pattern` — **promoted, deduped rules.** No date prefix. These are timeless: "Jason prefers concise communication." Append here only when a rule is stable.
- `## Observations` — **timestamped raw observations.** Date-prefixed: `- 2026-06-06: Jason chose X over Y because Z.` This is where new evidence goes by default.

During monthly Vault Health or when an observation stabilizes, promote it from `## Observations` into `## Fact / Pattern` (drop the date) and remove the duplicate from Observations. Trim Observations to the last ~30 entries when it grows past that — the rest live in session logs.

## What This Skill Does Not Do

- Does not replace reasoning. The vault is reference material — apply judgment.
- Does not auto-summarize the whole vault. Read targeted files, not everything.
- Does not store passwords or secrets, and never writes the API key into a vault note. If asked to "remember my password is X", decline and suggest a password manager.
