1
0
forked from jason/echo
Files
chorus/echo-memory-0.7.0.plugin
T

2606 lines
139 KiB
Plaintext
Raw Normal View History

2026-06-19 21:12:14 -05:00
PK
!©Ó\kÝžÜww README.md# echo-memory
Persistent memory for Claude via the **ECHO** Obsidian vault, using the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api).
Reads and writes notes across Claude/CoWork sessions using direct REST calls — no MCP server required, but routed through a bundled validated client (`scripts/echo.sh`) that status-checks every write. Built for **Jason Stedwell** (Director of Technical Services / Systems Engineer, MPM / ALABAMA wISP), who is both the operator and the architect of this vault.
**The plugin is the single source of truth.** All control logic — bootstrap/repair, operating contract, taxonomy, frontmatter conventions, and the canonical note templates — ships inside this plugin under `skills/echo-memory/`. The vault itself holds **data only**: there are no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` control docs in it. This makes ECHO self-bootstrapping (point it at an empty Obsidian vault and it stands up the full structure), easy to update (update the plugin, not the vault), and portable to any other vault.
## What it does
- Loads operator preferences, current context, and relevant project notes at the start of substantive conversations
- Writes facts, preferences, and decisions Jason asks Claude to remember
- Logs working sessions so future conversations can pick up where they left off
- **Bootstraps an empty vault from the bundled `scaffold/`** (folders, templates, anchor seeds, marker) and repairs/migrates an existing one via `scripts/bootstrap.sh` / `scripts/migrate.sh` — see `skills/echo-memory/references/bootstrap.md`
- Exposes `/echo-load`, `/echo-save`, `/echo-triage`, `/echo-health` slash commands as explicit entry points, and coordinates concurrent Claude/CoWork sessions via a cooperative advisory lock
## Configuration
The plugin is hardcoded for:
- **Server:** `https://echoapi.alwisp.com` (reverse proxy → backend Obsidian Local REST API)
- **API Key:** stored in the skill (personal plugin, not for distribution)
The endpoint presents a **valid TLS certificate**, so `-k` is not required. The bearer key lives only in the plugin — never inside the vault (per the vault's own safety rules).
## Vault layout (root-addressed)
```
/vault/
├── README.md ↠thin human signpost (not read for routing)
├── inbox/ (captures, imports, processing-log)
├── journal/ (daily, weekly, monthly, quarterly, annual, templates) — one time-series stream; rollups live here, not in a separate reviews/ tree
├── projects/ (active, incubating, on-hold, archived)
├── areas/ (business, personal, learning, systems)
├── resources/ (companies, concepts, references, people, meetings)
├── decisions/ (by-date)
└── _agent/
├── echo-vault.md ↠bootstrap marker (schema_version + date)
├── context/ ↠task-scoped context bundles
├── memory/ ↠working / episodic / semantic
├── sessions/ ↠YYYY-MM-DD-HHMM-<slug>.md
├── health/ ↠YYYY-MM-vault-health.md (monthly self-maintenance audit)
├── templates/ ↠canonical note templates (seeded from the plugin's scaffold/)
├── skills/ ↠active / archived
├── heartbeat/ ↠last-session.md orientation pointer (read at load, written at session end)
└── locks/ ↠vault.lock — cooperative advisory multi-writer lock
```
Control logic and the master scaffold live in the plugin, not the vault:
```
commands/ ↠slash commands: echo-load, echo-save, echo-triage, echo-health
skills/echo-memory/
├── SKILL.md ↠operating procedure (authoritative)
├── references/
│ ├── operating-contract.md ↠durable principles + safety + concurrency
│ ├── bootstrap.md ↠bootstrap / repair / migrate an empty vault
│ ├── vault-layout.md ↠canonical layout + frontmatter
│ ├── routing-map.md ↠complete endpoint→logic map (human authority)
│ ├── api-reference.md ↠REST endpoint patterns + routing map
│ └── session-log-template.md
├── scripts/
│ ├── echo.sh ↠validated API client (auth, status-check, retry, verify, lock)
│ ├── routing.json ↠canonical machine-readable route manifest (linter enforces it)
│ ├── vault-lint.sh ↠read-only invariant checker (monthly Vault Health pass)
│ ├── bootstrap.sh ↠deterministic, idempotent vault setup/repair
│ └── migrate.sh ↠deterministic schema migration (dry-run by default)
└── scaffold/ ↠verbatim files the bootstrap writes into the vault
├── README.vault.md echo-vault.md
├── templates/ (8 note templates)
└── anchors/ (operator-preferences, current-context, inbox seeds)
```
## Skills & commands
| Skill | Triggers |
|-------|----------|
| `echo-memory` | "remember that", "save to memory", "what do you know about me", "load my profile", "check my notes", "log this decision", "add to my inbox" — and proactively at the start of substantive conversations |
| Command | Does |
|---------|------|
| `/echo-load` | Cold-start context read (profile, scope, latest session, today, inbox) |
| `/echo-save <text>` | Route + persist content to its canonical home (search-first, idempotent) |
| `/echo-triage` | Drain aging inbox captures to their homes, logging each move |
| `/echo-health` | Run `vault-lint.sh` and summarize invariant violations |
## Requirements
- Obsidian running on the backend with the [Local REST API plugin](https://github.com/coddingtonbear/obsidian-local-rest-api) enabled
- HTTPS access to `https://echoapi.alwisp.com` from the Claude/CoWork session environment
PK
ĤÓ\.claude-plugin/PK
ĤÓ\²WÒD.claude-plugin/plugin.json{
"name": "echo-memory",
"version": "0.7.0",
"description": "Persistent memory via the ECHO Obsidian vault over the Local REST API. Self-bootstrapping: the plugin carries the full vault scaffold and all control logic (no in-vault routing docs), so it stands up an empty vault and ports cleanly to any other. Ships a validated API client (echo.sh), a canonical machine-readable routing manifest, deterministic bootstrap/migrate scripts, a read-only vault linter, and /echo-load|save|triage|health commands. Reads and writes notes across Claude/CoWork sessions using direct REST calls \u2014 no MCP server required. Jason's personal memory vault.",
"author": {
"name": "Jason"
},
"keywords": [
"memory",
"obsidian",
"notes",
"persistence",
"echo"
]
}PK
Œ¤Ó\ commands/PK
¤Ó\Ìsõ’èècommands/echo-load.md---
description: Load ECHO memory — cold-start context read (profile, scope, latest session, today, inbox)
---
Use the **echo-memory** skill to load memory now. Run the cold-start **Loading procedure** from `SKILL.md`: issue the 5–6 reads in parallel (marker, operator-preferences, current-context, heartbeat→latest session, today's daily note, inbox), then do the load-time **reconcile** (inbox-depth + scope-drift) and surface it in a single line.
Prefer the bundled client for each read:
```bash
"${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.sh" get _agent/memory/semantic/operator-preferences.md
```
Do not narrate the reads. End with a one-line orientation: who/what/where the active scope is, and any reconcile prompt.
PK
Œ¤Ó\%â¨HTTcommands/echo-health.md---
description: Run the ECHO vault-health linter and summarize any invariant violations
allowed-tools: Bash(*/echo-memory/scripts/vault-lint.sh*)
---
Run the bundled, read-only vault linter and report findings. Pass the conversation's current date so stale/aging math uses the same clock the agent writes with:
```bash
ECHO_TODAY=$(date +%F) "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/vault-lint.sh"
```
Exit codes: `0` clean · `1` violations (printed, grouped by check) · `2` vault unreachable · `3` vault not bootstrapped (run `bootstrap.sh`).
Summarize the violations grouped by category and propose fixes, but **do not auto-fix** without Jason's go-ahead. If this is the first substantive session of a calendar month, offer to write the findings to `_agent/health/YYYY-MM-vault-health.md` (per the skill's **Vault Health** section).
PK
‰¤Ó\-YO>KKcommands/echo-triage.md---
description: Triage the ECHO inbox — route aging captures to their canonical homes and log the moves
---
Use the **echo-memory** skill to run **Inbox Triage**. GET `inbox/captures/inbox.md`, list captures older than ~7 days that were never routed, and offer to route them.
```bash
"${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.sh" get inbox/captures/inbox.md
```
For each capture Jason accepts: send it to its proper home per the routing map (preference → `operator-preferences.md::Observations`; project idea → `projects/incubating/`; durable fact → `_agent/memory/semantic/`; person → `resources/people/`), then record the move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original> → <destination>`). Do **not** delete the original capture unless Jason explicitly asks — the processing log is the audit trail.
PK
†¤Ó\
µ®llcommands/echo-save.md---
description: Save to ECHO memory — route content to its canonical home (search-first, idempotent)
argument-hint: "[what to remember]"
---
Use the **echo-memory** skill to persist this to the ECHO vault:
> $ARGUMENTS
Follow the skill's write discipline exactly:
1. **Route** via `references/routing-map.md` (the canonical map). If no path fits, capture to `inbox/captures/inbox.md`.
2. **Search-first** for any new slug-addressed note (slug AND human title) before creating — merge/promote instead of duplicating.
3. Write through the bundled client so the call is status-checked and idempotent:
```bash
ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.sh"
"$ECHO" append inbox/captures/inbox.md "- $(date +%F): <entry>" # idempotent capture
"$ECHO" put <routed/path>.md <bodyfile> # create/overwrite (verifies)
"$ECHO" patch <path>.md append heading "<H1::Sub>" <bodyfile> # targeted append
```
4. Write in third person about Jason; set `agent_written: true` + `source_notes`; never put `[[wikilinks]]` in frontmatter; `bump` `updated:` only on substantive changes.
PK
<¡Ó\skills/PK
¥Ó\skills/echo-memory/PK
½¤Ó\skills/echo-memory/references/PK
²¤Ó\«Á}¤+¤+,skills/echo-memory/references/routing-map.md# ECHO 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.sh` 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`. 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 Jason 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 Jason 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 Jason 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 |
**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/echo-vault.md` | Bootstrap / schema migration only | Marker: `schema_version`, bootstrap date | Plugin-owned probe; never hand-edited | GET / PUT |
| `_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 Jason | 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 a Jason-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 Jason'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` | Jason 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 |
---
## 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 Jason'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.
PK
<¡Ó\bCIÒ›!!-skills/echo-memory/references/vault-layout.md# Vault Layout & Frontmatter Conventions
**This document is canonical.** The ECHO 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/
├── echo-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)
```
**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.
```yaml
---
type: # see Note Types below
status: # active | draft | done | archived | complete
created: # YYYY-MM-DD (or YYYY-MM-DDTHH:mm for sessions)
updated: # YYYY-MM-DD
tags: []
agent_written: false
source_notes: [] # plain relative paths as 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 Jason 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 Jason — he is both operator and architect of this vault.
### 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`. ECHO 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. `jason-stedwell.md`).
### companies/\<slug\>.md
`type: company`. An organization Jason works with — client, vendor, partner, or employer (e.g. `gillig.md`, `mpm.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.
PK
<¡Ó\É9väY Y 5skills/echo-memory/references/session-log-template.md# 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 2–5 words, kebab-case.
Examples: `2026-06-05-1430-echo-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 echo-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 — ECHO is a dedicated vault.
- Key hardcoded in the plugin (not in the vault) — personal plugin, per the reference pattern.
## Outputs Created
- `echo-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]]`.
PK
¯¤Ó\å,ò^%%*skills/echo-memory/references/bootstrap.md# Bootstrap & Repair
The **plugin is the single source of truth** for ECHO'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 ECHO 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/echo-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/echo-memory/scripts"
"$SCRIPTS/bootstrap.sh" --dry-run # preview what would be seeded
"$SCRIPTS/bootstrap.sh" # idempotent, additive — fills only what is missing (also the repair path)
"$SCRIPTS/migrate.sh" # plan a schema migration (dry-run)
"$SCRIPTS/migrate.sh" --apply # perform the migration (moves/deletes, after review)
```
`bootstrap.sh` writes through `echo.sh`, so every step is status-checked and the marker is written last. The manual steps below document what the script does (and serve as a fallback if the script can't run).
```
AUTH="Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab"
BASE="https://echoapi.alwisp.com"
```
---
## 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/echo-vault.md"
```
- **200** → bootstrapped. Read the marker's `schema_version`; if it is **less than** the plugin's current schema (2), 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 echo-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/echo-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/echo-vault.md` → `/vault/_agent/echo-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 1–5, 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.sh` 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/echo-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. *(Jason's live vault was hand-migrated for the one existing `2026-W24` artifact on 2026-06-10; this step covers any vault bootstrapped under schema 1.)*
PK
½¤Ó\TóIº``3skills/echo-memory/references/operating-contract.md# ECHO — Operating Contract
The durable, client-independent contract for any agent operating against the ECHO 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 future REST/MCP 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, MCP-compatible tools, and REST API clients.
## 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/MCP 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 (`echo.sh lock <id>` → `_agent/locks/vault.lock`) and release it at session end. The lock is cooperative with a TTL (stale locks are reclaimable); it is a courtesy, not a hard mutex.
- Idempotent append (read-before-POST, via `echo.sh 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.
PK
·¤Ó\ÿŒhK"K".skills/echo-memory/references/api-reference.md# ECHO — Obsidian Local REST API Reference
Server: `https://echoapi.alwisp.com` (reverse proxy → backend Obsidian Local REST API)
Auth header: `Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab`
The endpoint has a **valid TLS certificate** — `-k` is not required. Paths address the vault at its **root**.
> **Prefer `scripts/echo.sh` over the raw recipes below.** It wraps every verb with auth, status checking, retry, idempotent append, and frontmatter patches. The recipes here are the underlying mechanics and the fallback. **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.
---
## Reading Files
```bash
# Read any file by vault path
curl -s \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/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 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/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 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/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 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_entry.md \
"https://echoapi.alwisp.com/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 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_file.md \
"https://echoapi.alwisp.com/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'
Jason prefers concise status updates — lead with the decision.
OBSEOF
curl -s -X PATCH \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Operation: append" \
-H "Target-Type: heading" \
-H "Target: Operator Preferences::Fact / Pattern" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_patch.md \
"https://echoapi.alwisp.com/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 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Accept: application/vnd.olrapi.document-map+json" \
"https://echoapi.alwisp.com/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
```bash
curl -s -X PATCH \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Operation: replace" \
-H "Target-Type: frontmatter" \
-H "Target: updated" \
-H "Content-Type: application/json" \
--data '"2026-06-05"' \
"https://echoapi.alwisp.com/vault/projects/active/vault-foundation.md"
```
---
## Searching
```bash
curl -s -X POST \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/search/simple/?query=weekly+review"
```
Returns an array of `{ filename, score, matches: [{ context, match }] }`.
---
## Deleting Files
```bash
curl -s -X DELETE \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/vault/inbox/imports/old-note.md"
```
Only on explicit operator request. Deletion is destructive.
---
## 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/echo-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`).
PK
q¤Ó\skills/echo-memory/scripts/PK
ò£Ó\% O„²²%skills/echo-memory/scripts/migrate.sh#!/usr/bin/env bash
# migrate.sh — bring an existing ECHO vault up to the plugin's current schema.
#
# Reads the marker's schema_version and applies each intervening migration in order.
# Migrations are idempotent and additive; every destructive step (DELETE) is gated
# behind --apply AND prints what it will do first. Default mode is a DRY-RUN plan.
#
# Usage:
# migrate.sh # print the migration plan (no changes)
# migrate.sh --apply # perform the migration (moves/deletes included)
#
# Env: ECHO_BASE, ECHO_KEY (via echo.sh).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ECHO="$SCRIPT_DIR/echo.sh"
CURRENT_SCHEMA=2
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1
[ -x "$ECHO" ] || chmod +x "$ECHO" 2>/dev/null || true
say() { echo "migrate: $*"; }
do_or_show() { # do_or_show "<human description>" cmd args...
local desc="$1"; shift
if [ "$APPLY" = "1" ]; then say "APPLY $desc"; "$@"; else say "PLAN $desc"; fi
}
# ---- Read current schema -----------------------------------------------------
if ! marker="$("$ECHO" get _agent/echo-vault.md 2>/dev/null)"; then
say "marker missing — vault not bootstrapped. Run bootstrap.sh, not migrate.sh."
exit 3
fi
FROM="$(printf '%s' "$marker" | sed -n 's/^schema_version:[[:space:]]*//p' | head -1)"
FROM="${FROM:-0}"
say "vault schema_version=$FROM, plugin schema=$CURRENT_SCHEMA $([ "$APPLY" = 1 ] && echo '(APPLY)' || echo '(dry-run)')"
if [ "$FROM" -ge "$CURRENT_SCHEMA" ] 2>/dev/null; then
say "up to date — nothing to do."
exit 0
fi
ls_files() { "$ECHO" ls "$1" 2>/dev/null | python3 -c 'import sys,json;print("\n".join(json.load(sys.stdin).get("files",[])))' 2>/dev/null || true; }
move() { # move SRC DST preserving content (PUT dst <- get src, then delete src)
local src="$1" dst="$2"
"$ECHO" get "$src" 2>/dev/null | "$ECHO" put "$dst" - >/dev/null
"$ECHO" delete "$src" >/dev/null
}
# ---- 0 -> 1 : control docs moved into the plugin -----------------------------
mig_0_1() {
say "[0->1] retire in-vault control docs (CLAUDE/BOOTSTRAP/STRUCTURE/index.md)"
for f in CLAUDE.md BOOTSTRAP.md STRUCTURE.md index.md; do
if ECHO_VERIFY=0 "$ECHO" get "$f" >/dev/null 2>&1; then
do_or_show "delete vault/$f (back it up outside the vault first)" "$ECHO" delete "$f"
fi
done
say "[0->1] reminder: scrub dangling [[CLAUDE]]/[[BOOTSTRAP]]/[[STRUCTURE]]/[[index]] links from ## Related sections (manual/agent step)."
}
# ---- 1 -> 2 : reviews/ folded into journal/ + _agent/health/ -----------------
mig_1_2() {
say "[1->2] fold reviews/ into journal/ and _agent/health/"
for f in $(ls_files reviews/weekly); do
[ "${f%.md}" != "$f" ] || continue
dst="journal/weekly/$(printf '%s' "$f" | sed 's/-review\.md$/.md/')"
do_or_show "move reviews/weekly/$f -> $dst" move "reviews/weekly/$f" "$dst"
done
for f in $(ls_files reviews/monthly); do
[ "${f%.md}" != "$f" ] || continue
case "$f" in
*vault-health.md) dst="_agent/health/$f" ;;
*) dst="journal/monthly/$f" ;;
esac
do_or_show "move reviews/monthly/$f -> $dst" move "reviews/monthly/$f" "$dst"
done
for period in quarterly annual; do
for f in $(ls_files "reviews/$period"); do
[ "${f%.md}" != "$f" ] || continue
do_or_show "move reviews/$period/$f -> journal/$period/$f" move "reviews/$period/$f" "journal/$period/$f"
done
done
say "[1->2] reminder: update inbound [[reviews/...]] wikilinks in ## Related sections (manual/agent step)."
}
[ "$FROM" -lt 1 ] && mig_0_1
[ "$FROM" -lt 2 ] && mig_1_2
# ---- Stamp the marker --------------------------------------------------------
do_or_show "set _agent/echo-vault.md schema_version -> $CURRENT_SCHEMA" \
"$ECHO" fm _agent/echo-vault.md schema_version "$CURRENT_SCHEMA"
if [ "$APPLY" = "1" ]; then
say "migration complete -> schema $CURRENT_SCHEMA. Run vault-lint.sh to confirm invariants."
else
say "dry-run only. Re-run with --apply to perform the moves/deletes above."
fi
PK
,¤Ó\µ—ëü!ü!"skills/echo-memory/scripts/echo.sh#!/usr/bin/env bash
# echo.sh — the single validated client for the ECHO Obsidian Local REST API.
#
# Every read/write to the vault should go through this script instead of hand-built
# curl. It centralizes: auth injection, HTTP-status checking (non-zero exit on >=400
# so a failed write can never look like success), one bounded retry on 5xx/connection
# errors, idempotent append (read-before-POST), correct `::` heading-target handling,
# frontmatter field patches, and an advisory multi-writer lock.
#
# Config (env overrides; defaults match the rest of the plugin):
# ECHO_BASE default https://echoapi.alwisp.com
# ECHO_KEY default the plugin bearer token
# ECHO_VERIFY default 1 — read-back verify after a PUT
# ECHO_LOCK_TTL default 900 — seconds before an advisory lock is considered stale
#
# Usage:
# echo.sh get <path> # print file contents (404 -> exit 44)
# echo.sh map <path> # document-map JSON (headings/blocks/frontmatter)
# echo.sh ls <dir> # directory listing JSON
# echo.sh search <query...> # /search/simple
# echo.sh put <path> [file] # create/overwrite (body from file or stdin)
# echo.sh post <path> [file] # raw append (NON-idempotent; prefer `append`)
# echo.sh append <path> <line> # idempotent append: skips if the exact line exists
# echo.sh patch <path> <append|prepend|replace> <heading|frontmatter|block> <target> [file]
# echo.sh fm <path> <field> <json-value> # PATCH a frontmatter scalar (e.g. fm p.md updated '"2026-06-19"')
# echo.sh bump <path> [YYYY-MM-DD] # set frontmatter updated: to today (or given date)
# echo.sh delete <path> # DELETE (destructive; explicit use only)
# echo.sh lock <owner-id> # acquire advisory lock (exit 75 if held by someone else & fresh)
# echo.sh unlock <owner-id> # release advisory lock if owned by <owner-id>
#
# Exit codes: 0 ok · 44 not-found(404) · 75 lock-held · 2 usage · 1 other HTTP/transport error.
set -euo pipefail
ECHO_BASE="${ECHO_BASE:-https://echoapi.alwisp.com}"
ECHO_BASE="${ECHO_BASE%/}"
ECHO_KEY="${ECHO_KEY:-241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab}"
ECHO_VERIFY="${ECHO_VERIFY:-1}"
ECHO_LOCK_TTL="${ECHO_LOCK_TTL:-900}"
AUTH="Authorization: Bearer ${ECHO_KEY}"
# response-body scratch file (filled by every _curl; read by callers via $RESP)
RESP="$(mktemp)"; BODY=""
cleanup() { rm -f "$RESP" "${BODY:-}"; }
trap cleanup EXIT
die() { echo "echo.sh: $*" >&2; exit 1; }
usage() { sed -n '2,40p' "$0" >&2; exit 2; }
_today() { echo "${ECHO_TODAY:-$(date +%Y-%m-%d)}"; }
_now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
_vault_url() { echo "${ECHO_BASE}/vault/$1"; }
# _curl METHOD URL [extra curl args...]
# Writes the response body to $RESP and the status code to $HTTP — BOTH IN THE PARENT
# SHELL (never call this in $(...) or on the right of a pipe, or those globals are lost).
# One bounded retry on transport failure (000) or 5xx.
HTTP=""
_curl() {
local method="$1" url="$2"; shift 2
local code attempt=0
while :; do
code="$(curl -sS -X "$method" -H "$AUTH" -o "$RESP" -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000)"
if { [ "$code" = "000" ] || [ "${code:0:1}" = "5" ]; } && [ "$attempt" -lt 1 ]; then
attempt=$((attempt+1)); sleep 1; continue
fi
break
done
HTTP="$code"
}
# assert $HTTP is acceptable; 404 -> exit 44, other >=400 -> exit 1
_check() {
local ctx="$1"
[ "$HTTP" = "404" ] && exit 44
[ "$HTTP" = "000" ] && die "$ctx: vault unreachable (connection failed) [$ECHO_BASE]"
[ "${HTTP:-000}" -ge 400 ] && die "$ctx: HTTP $HTTP — $(cat "$RESP")"
return 0
}
# capture a body argument (file path or '-'/empty for stdin) into $BODY (a temp file)
_capture_body() {
BODY="$(mktemp)"
if [ "${1:-}" = "" ] || [ "${1:-}" = "-" ]; then cat > "$BODY"; else cat "$1" > "$BODY"; fi
}
cmd="${1:-}"; shift || true
case "$cmd" in
get)
[ $# -ge 1 ] || usage
_curl GET "$(_vault_url "$1")"; _check "get $1"; cat "$RESP" ;;
map)
[ $# -ge 1 ] || usage
_curl GET "$(_vault_url "$1")" -H 'Accept: application/vnd.olrapi.document-map+json'
_check "map $1"; cat "$RESP" ;;
ls)
[ $# -ge 1 ] || usage
p="$1"; [ "${p%/}" = "$p" ] && p="$p/"
_curl GET "$(_vault_url "$p")"; _check "ls $1"; cat "$RESP" ;;
search)
[ $# -ge 1 ] || usage
q="$*"; q="${q// /+}"
_curl POST "${ECHO_BASE}/search/simple/?query=${q}"; _check "search"; cat "$RESP" ;;
put)
[ $# -ge 1 ] || usage
path="$1"; _capture_body "${2:-}"
_curl PUT "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
_check "put $path"
if [ "$ECHO_VERIFY" = "1" ]; then
_curl GET "$(_vault_url "$path")"
[ "$HTTP" = "200" ] || die "put $path: write did not verify (GET returned $HTTP)"
fi
echo "ok: PUT $path" ;;
post)
[ $# -ge 1 ] || usage
path="$1"; _capture_body "${2:-}"
_curl POST "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
_check "post $path"; echo "ok: POST $path" ;;
append)
# idempotent: GET the file, skip the POST if the exact line is already present.
[ $# -ge 2 ] || usage
path="$1"; line="$2"
_curl GET "$(_vault_url "$path")"
if [ "$HTTP" = "200" ] && grep -qF -- "$line" "$RESP"; then
echo "skip: line already present in $path"; exit 0
fi
[ "$HTTP" = "200" ] || [ "$HTTP" = "404" ] || _check "append(read) $path"
BODY="$(mktemp)"; printf '%s\n' "$line" > "$BODY"
_curl POST "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
_check "append $path"; echo "ok: APPEND $path" ;;
patch)
[ $# -ge 4 ] || usage
path="$1"; op="$2"; ttype="$3"; target="$4"; _capture_body "${5:-}"
case "$op" in append|prepend|replace) ;; *) die "patch: op must be append|prepend|replace";; esac
case "$ttype" in heading|frontmatter|block) ;; *) die "patch: target-type must be heading|frontmatter|block";; esac
_curl PATCH "$(_vault_url "$path")" \
-H "Operation: $op" -H "Target-Type: $ttype" -H "Target: $target" \
-H 'Content-Type: text/markdown' --data-binary @"$BODY"
_check "patch $path ($target)"
echo "ok: PATCH $op $ttype '$target' -> $path" ;;
fm)
[ $# -ge 3 ] || usage
path="$1"; field="$2"; value="$3"
BODY="$(mktemp)"; printf '%s' "$value" > "$BODY"
_curl PATCH "$(_vault_url "$path")" \
-H 'Operation: replace' -H 'Target-Type: frontmatter' -H "Target: $field" \
-H 'Content-Type: application/json' --data-binary @"$BODY"
_check "fm $path.$field"; echo "ok: frontmatter $field -> $path" ;;
bump)
[ $# -ge 1 ] || usage
path="$1"; d="${2:-$(_today)}"
exec "$0" fm "$path" updated "\"$d\"" ;;
delete)
[ $# -ge 1 ] || usage
_curl DELETE "$(_vault_url "$1")"; _check "delete $1"; echo "ok: DELETE $1" ;;
lock)
# advisory lock: _agent/locks/vault.lock holds "<owner> @ <iso>". Honored cooperatively.
[ $# -ge 1 ] || usage
owner="$1"; lockpath="_agent/locks/vault.lock"
_curl GET "$(_vault_url "$lockpath")"
if [ "$HTTP" = "200" ] && [ -s "$RESP" ]; then
cur="$(cat "$RESP")"; held_owner="${cur%% @ *}"; held_iso="${cur##* @ }"; held_iso="${held_iso%$'\n'}"
held_epoch="$(date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$held_iso" +%s 2>/dev/null \
|| date -u -d "$held_iso" +%s 2>/dev/null || echo 0)"
now_epoch="$(date -u +%s)"
if [ "$held_owner" != "$owner" ] && [ $((now_epoch - held_epoch)) -lt "$ECHO_LOCK_TTL" ]; then
echo "lock held by '$held_owner' since $held_iso (fresh)" >&2; exit 75
fi
fi
BODY="$(mktemp)"; printf '%s @ %s\n' "$owner" "$(_now_iso)" > "$BODY"
_curl PUT "$(_vault_url "$lockpath")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
_check "lock"; echo "ok: locked by $owner" ;;
unlock)
[ $# -ge 1 ] || usage
owner="$1"; lockpath="_agent/locks/vault.lock"
_curl GET "$(_vault_url "$lockpath")"
if [ "$HTTP" = "200" ]; then
cur="$(cat "$RESP")"; held_owner="${cur%% @ *}"
[ "$held_owner" = "$owner" ] || { echo "lock owned by '$held_owner', not '$owner' — not releasing" >&2; exit 75; }
fi
_curl DELETE "$(_vault_url "$lockpath")"
[ "$HTTP" = "404" ] || _check "unlock"
echo "ok: unlocked" ;;
""|-h|--help|help) usage ;;
*) die "unknown command '$cmd' (try: get map ls search put post append patch fm bump delete lock unlock)" ;;
esac
PK
æ£Ó\ßNNjj'skills/echo-memory/scripts/bootstrap.sh#!/usr/bin/env bash
# bootstrap.sh — stand up (or repair) an ECHO vault deterministically.
#
# Idempotent and additive: every write is probe-before-write and NEVER overwrites an
# existing file. The marker (_agent/echo-vault.md) is written LAST, so the vault is
# only flagged "set up" once every piece is in place. Safe to re-run any time — that
# is also the "repair" path (it fills in only what is missing).
#
# All scaffold is resolved relative to THIS script's location, so it works regardless
# of the caller's CWD (fixes the old `@scaffold/...` relative-path assumption).
#
# Usage:
# bootstrap.sh [--dry-run]
#
# Env: ECHO_BASE, ECHO_KEY (passed through to echo.sh), ECHO_TODAY (YYYY-MM-DD for {{DATE}}).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
SCAFFOLD="$SKILL_DIR/scaffold"
ECHO="$SCRIPT_DIR/echo.sh"
TODAY="${ECHO_TODAY:-$(date +%Y-%m-%d)}"
DRY=0
[ "${1:-}" = "--dry-run" ] || [ "${1:-}" = "-n" ] && DRY=1
[ -d "$SCAFFOLD" ] || { echo "bootstrap: scaffold not found at $SCAFFOLD" >&2; exit 1; }
[ -x "$ECHO" ] || chmod +x "$ECHO" 2>/dev/null || true
say() { echo "bootstrap: $*"; }
exists() { ECHO_VERIFY=0 "$ECHO" get "$1" >/dev/null 2>&1; } # 0 = present(200), nonzero = absent/404
# seed VAULT_PATH from LOCAL_FILE (with {{DATE}} substitution), only if absent
seed() {
local vpath="$1" local_file="$2"
if exists "$vpath"; then say "skip (exists) $vpath"; return 0; fi
if [ "$DRY" = "1" ]; then say "would seed $vpath <- ${local_file#$SKILL_DIR/}"; return 0; fi
sed "s/{{DATE}}/$TODAY/g" "$local_file" | ECHO_VERIFY=1 "$ECHO" put "$vpath" - >/dev/null
say "seeded $vpath"
}
# write a one-line leaf README only if absent
leaf_readme() {
local dir="$1" name="${1##*/}"
local vpath="$dir/README.md"
if exists "$vpath"; then return 0; fi
if [ "$DRY" = "1" ]; then say "would readme $vpath"; return 0; fi
printf '# %s\n\nMemory vault folder. See the echo-memory plugin for conventions.\n' "$name" \
| ECHO_VERIFY=0 "$ECHO" put "$vpath" - >/dev/null
say "readme $vpath"
}
# ---- Pre-flight: is the vault already bootstrapped? --------------------------
if exists "_agent/echo-vault.md"; then
ver="$("$ECHO" get _agent/echo-vault.md 2>/dev/null | sed -n 's/^schema_version:[[:space:]]*//p' | head -1)"
say "marker present (schema_version=${ver:-unknown}). Running repair pass (fills only missing files)."
CUR_SCHEMA=2
if [ -n "$ver" ] && [ "$ver" -lt "$CUR_SCHEMA" ] 2>/dev/null; then
say "NOTE: schema_version $ver < $CUR_SCHEMA — run migrate.sh before relying on the vault."
fi
fi
# ---- 1. Folder tree (leaf READMEs guarantee non-empty dirs) ------------------
LEAVES=(
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 _agent/locks
)
for d in "${LEAVES[@]}"; do leaf_readme "$d"; done
# ---- 2. Templates (mirror scaffold/templates/ 1:1 into the vault) ------------
if [ -d "$SCAFFOLD/templates" ]; then
while IFS= read -r f; do
rel="${f#$SCAFFOLD/templates/}"
seed "$rel" "$f"
done < <(find "$SCAFFOLD/templates" -type f -name '*.md')
fi
# ---- 3. Anchor seeds (only if absent — never fabricate facts) ----------------
seed "_agent/memory/semantic/operator-preferences.md" "$SCAFFOLD/anchors/operator-preferences.seed.md"
seed "_agent/context/current-context.md" "$SCAFFOLD/anchors/current-context.seed.md"
seed "inbox/captures/inbox.md" "$SCAFFOLD/anchors/inbox.seed.md"
# ---- 4. Vault README (human signpost) ----------------------------------------
seed "README.md" "$SCAFFOLD/README.vault.md"
# ---- 5. Marker (write LAST) --------------------------------------------------
seed "_agent/echo-vault.md" "$SCAFFOLD/echo-vault.md"
say "done (${DRY:+DRY-RUN }$TODAY)."
say "Next: create today's daily note + a bootstrap session log + heartbeat (see SKILL.md First-run trace)."
PK
q¤Ó\О~öº-º-(skills/echo-memory/scripts/vault-lint.sh#!/usr/bin/env bash
# vault-lint.sh — mechanically assert ECHO vault invariants.
#
# Catches the recurring "invariant violation" bugs that prose rules can't enforce:
# folder<->status drift, duplicate slugs, wikilinks leaking into frontmatter,
# duplicate "## Agent Log" headings, stale active projects, aging inbox captures,
# impossible dates, bad status values, missing frontmatter, broken source_notes, and
# paths that no route in routing.json permits. Invoked by the monthly Vault Health
# pass (see SKILL.md), but safe to run any time — it is READ-ONLY.
#
# Exit status: 0 = clean, 1 = violations found, 2 = vault unreachable,
# 3 = vault not bootstrapped (marker missing).
#
# Config (env overrides):
# ECHO_BASE (default https://echoapi.alwisp.com)
# ECHO_KEY (default the plugin's bearer token)
# ECHO_TODAY (default the machine date) — pass the conversation's currentDate so
# stale/aging math uses the SAME clock the agent writes with (YYYY-MM-DD)
# STALE_DAYS (default 30) INBOX_DAYS (default 14)
#
# routing.json (canonical route manifest) is read from this script's own directory.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ECHO_BASE="${ECHO_BASE:-https://echoapi.alwisp.com}"
ECHO_KEY="${ECHO_KEY:-241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab}"
STALE_DAYS="${STALE_DAYS:-30}"
INBOX_DAYS="${INBOX_DAYS:-14}"
ECHO_TODAY="${ECHO_TODAY:-$(date +%Y-%m-%d)}"
ECHO_BASE="$ECHO_BASE" ECHO_KEY="$ECHO_KEY" STALE_DAYS="$STALE_DAYS" INBOX_DAYS="$INBOX_DAYS" \
ECHO_TODAY="$ECHO_TODAY" ROUTING_JSON="$SCRIPT_DIR/routing.json" \
python3 - <<'PY'
import os, sys, json, re, datetime, urllib.request, urllib.error
BASE = os.environ["ECHO_BASE"].rstrip("/")
KEY = os.environ["ECHO_KEY"]
STALE_DAYS = int(os.environ["STALE_DAYS"])
INBOX_DAYS = int(os.environ["INBOX_DAYS"])
TODAY = datetime.date.fromisoformat(os.environ["ECHO_TODAY"])
ROUTING_JSON = os.environ["ROUTING_JSON"]
LIFECYCLES = ["active", "incubating", "on-hold", "archived"]
SKIP = {"README.md", "project-template.md", "decision-template.md"}
REQUIRED_FM = ("type", "created")
# Project status vocabulary IS enforced (status must equal the lifecycle folder) by the
# folder/status check below. Other note kinds (decisions/concepts) carry free-form status
# vocab (accepted, shipped, reference, ...), so there is no global status allow-list.
# optional real YAML parser; fall back to a tolerant line parser
try:
import yaml # type: ignore
HAVE_YAML = True
except Exception:
HAVE_YAML = False
violations = []
def flag(check, msg): violations.append((check, msg))
def get(path):
"""GET /vault/<path>. Returns text, or None on 404. Raises on hard failure."""
req = urllib.request.Request(f"{BASE}/vault/{path}",
headers={"Authorization": f"Bearer {KEY}"})
try:
with urllib.request.urlopen(req, timeout=20) as r:
return r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise
def list_dir(path):
"""Return (files, folders) for a vault directory. Directories may arrive either in a
'folders' key OR as 'files' entries ending in '/'; handle both. Root is '' -> /vault/.
Tolerates non-404 errors (e.g. a 400 on an odd path) by returning empty."""
p = "" if path in ("", "/") else (path if path.endswith("/") else path + "/")
try:
body = get(p)
except urllib.error.HTTPError:
return [], []
if body is None:
return [], []
try:
j = json.loads(body)
except json.JSONDecodeError:
return [], []
entries = list(j.get("files", [])) + list(j.get("folders", []))
files = [e for e in entries if not e.endswith("/")]
folders = [e[:-1] for e in entries if e.endswith("/")]
return files, folders
def walk(prefix=""):
"""Yield every file path under prefix (recursive). prefix is '' or ends with '/'."""
files, folders = list_dir(prefix)
for f in files:
yield prefix + f
for d in folders:
yield from walk(f"{prefix}{d}/")
def split_frontmatter(text):
"""Return (raw_yaml_str, body) splitting on anchored ^---$ delimiters. ('', text) if none."""
if not text:
return "", ""
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return "", text
for i in range(1, len(lines)):
if lines[i].strip() == "---":
return "\n".join(lines[1:i]), "\n".join(lines[i+1:])
return "", text # unterminated block -> treat as no frontmatter
def parse_fm(text):
"""Return (raw_yaml_str, dict). Uses PyYAML when available, else a tolerant parser."""
raw, _ = split_frontmatter(text)
if not raw:
return "", {}
if HAVE_YAML:
try:
d = yaml.safe_load(raw)
return raw, (d if isinstance(d, dict) else {})
except Exception:
pass
# fallback: scalar + simple inline-list lines (keys may contain digits, _, -)
fields = {}
for line in raw.splitlines():
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
if m:
v = m.group(2).strip()
if v.startswith("[") and v.endswith("]"):
v = [x.strip().strip('"').strip("'") for x in v[1:-1].split(",") if x.strip()]
else:
v = v.strip('"').strip("'")
fields[m.group(1)] = v
return raw, fields
def parse_date(s):
m = re.match(r"(\d{4}-\d{2}-\d{2})", str(s or ""))
if not m:
return None
try:
return datetime.date.fromisoformat(m.group(1))
except ValueError:
return None
def as_list(v):
if v is None or v == "":
return []
return v if isinstance(v, list) else [v]
# ---- Reachability + bootstrap probe (M2: do NOT silently report clean) -------
try:
if get("_agent/echo-vault.md") is None:
print("vault-lint: marker missing — vault not bootstrapped (run bootstrap.sh).", file=sys.stderr)
sys.exit(3)
except Exception as e:
print(f"vault-lint: vault unreachable ({e}).", file=sys.stderr)
sys.exit(2)
# ---- Load canonical routing manifest (S3) ------------------------------------
ROUTES, RETIRED = [], []
try:
with open(ROUTING_JSON) as fh:
rj = json.load(fh)
ROUTES = [(r["id"], re.compile(r["pattern"])) for r in rj.get("routes", [])]
RETIRED = [(re.compile(r["pattern"]), r.get("replacement", "")) for r in rj.get("retired", [])]
except Exception as e:
flag("routing-manifest", f"could not load routing.json ({e}) — path checks skipped")
# ---- Single full walk feeds every path-level check ---------------------------
all_files = list(walk())
def route_for(path):
for rid, rx in ROUTES:
if rx.match(path):
return rid
return None
# Path membership + retired-path detection (S3)
for path in all_files:
if ROUTES and route_for(path) is None:
hit = next((repl for rx, repl in RETIRED if rx.match(path)), None)
if hit is not None:
flag("retired-path", f"{path}: retired location — should be {hit}")
else:
flag("unknown-path", f"{path}: matches no route in routing.json")
# ---- Per-note frontmatter checks (M5) ----------------------------------------
TEMPLATE_RE = re.compile(r"(^|/)(templates/|.*-template\.md$)")
for path in all_files:
base = path.rsplit("/", 1)[-1]
if base in SKIP or TEMPLATE_RE.search(path) or not path.endswith(".md"):
continue
text = get(path)
if text is None:
continue
raw, fm = parse_fm(text)
# wikilinks anywhere in frontmatter (widened sweep — all folders)
if "[[" in raw:
flag("frontmatter-wikilink", f"{path}: '[[...]]' inside frontmatter")
# missing required frontmatter
missing = [k for k in REQUIRED_FM if not str(fm.get(k, "")).strip()]
if fm and missing:
flag("missing-frontmatter", f"{path}: missing {', '.join(missing)}")
# impossible dates: updated < created
c, u = parse_date(fm.get("created")), parse_date(fm.get("updated"))
if c and u and u < c:
flag("date-order", f"{path}: updated {u} is before created {c}")
if u and u > TODAY:
flag("future-date", f"{path}: updated {u} is in the future (today {TODAY})")
# source_notes hygiene: plain relative paths, never wikilinks, no self-reference
for sn in as_list(fm.get("source_notes")):
s = str(sn)
if "[[" in s:
flag("source-notes-wikilink", f"{path}: source_notes contains a wikilink '{s}'")
# ---- Projects: folder<->status, stale active, duplicate slugs ----------------
slug_homes = {}
for lc in LIFECYCLES:
files, _ = list_dir(f"projects/{lc}")
for fn in files:
if fn.endswith("/") or fn in SKIP or not fn.endswith(".md"):
continue
slug = fn[:-3]
slug_homes.setdefault(slug, []).append(lc)
text = get(f"projects/{lc}/{fn}")
if text is None:
continue
_, fm = parse_fm(text)
status = str(fm.get("status", "")).strip().strip('"').strip("'")
if status and status != lc:
flag("folder/status", f"projects/{lc}/{fn}: status='{status}' but folder='{lc}'")
if lc == "active":
d = parse_date(fm.get("updated"))
if d and (TODAY - d).days > STALE_DAYS:
flag("stale-active", f"projects/active/{fn}: updated {d} ({(TODAY-d).days}d ago) — consider on-hold/")
for slug, homes in slug_homes.items():
if len(homes) > 1:
flag("duplicate-slug", f"'{slug}' exists in {', '.join(homes)}")
# ---- Daily notes: duplicate "## Agent Log" headings --------------------------
for path in all_files:
if not re.match(r"^journal/daily/.*\.md$", path):
continue
text = get(path) or ""
n = len(re.findall(r"(?m)^## Agent Log\s*$", text))
if n > 1:
flag("duplicate-agent-log", f"{path}: {n} '## Agent Log' headings")
# ---- Inbox: captures aging past INBOX_DAYS -----------------------------------
inbox = get("inbox/captures/inbox.md") or ""
for line in inbox.splitlines():
m = re.match(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\b", line)
if m:
d = parse_date(m.group(1))
if d and (TODAY - d).days > INBOX_DAYS:
flag("aging-inbox", f"inbox capture {d} ({(TODAY-d).days}d): {line.strip()[:80]}")
# ---- Report ------------------------------------------------------------------
if not violations:
print("vault-lint: clean — all invariants hold.")
sys.exit(0)
print(f"vault-lint: {len(violations)} violation(s) found\n")
by = {}
for check, msg in violations:
by.setdefault(check, []).append(msg)
labels = {
"folder/status": "Folder <-> status mismatch",
"duplicate-slug": "Duplicate slug across lifecycle folders",
"frontmatter-wikilink": "Wikilink in frontmatter (breaks reading view)",
"duplicate-agent-log": "Duplicate '## Agent Log' heading",
"stale-active": f"Stale active project (updated > {STALE_DAYS}d)",
"aging-inbox": f"Inbox capture aging (> {INBOX_DAYS}d)",
"unknown-path": "Path matches no route in routing.json",
"retired-path": "Write to a retired/dead path",
"missing-frontmatter": "Missing required frontmatter field",
"date-order": "updated earlier than created",
"future-date": "updated date is in the future",
"source-notes-wikilink": "Wikilink in source_notes (must be plain paths)",
"routing-manifest": "routing.json problem",
}
for check, msgs in by.items():
print(f"## {labels.get(check, check)}")
for m in msgs:
print(f" - {m}")
print()
sys.exit(1)
PY
PK
”£Ó\Á)ýp&p&'skills/echo-memory/scripts/routing.json{
"$comment": "CANONICAL machine-readable routing manifest for the ECHO vault. This is the single source of truth for 'what paths may be written to'. The human-readable tables in SKILL.md, references/routing-map.md, and references/api-reference.md are DERIVED views of this file — when they disagree, this file wins. vault-lint.sh consumes it to enforce the core rule: if a path matches no route here (and is not a retired path), nothing should be written to it. Patterns are Python regexes matched against vault-root-relative paths (no leading slash, no /vault/ prefix).",
"schema_version": 2,
"routes": [
{ "id": "inbox-captures", "pattern": "^inbox/captures/inbox\\.md$", "method": "POST", "trigger": "Destination unknown at capture time", "distinct_because": "Only path whose contract is deferred routing" },
{ "id": "inbox-imports", "pattern": "^inbox/imports/[^/]+\\.md$", "method": "PUT", "trigger": "Raw external material dropped wholesale", "distinct_because": "Bulk un-triaged material vs single-line captures" },
{ "id": "inbox-processing-log", "pattern": "^inbox/processing-log/\\d{4}-\\d{2}-\\d{2}\\.md$", "method": "POST", "trigger": "An inbox item is routed to its real home", "distinct_because": "Audit trail of moves, not memory itself" },
{ "id": "journal-daily", "pattern": "^journal/daily/\\d{4}-\\d{2}-\\d{2}\\.md$", "method": "PATCH", "trigger": "First agent activity on a given day", "distinct_because": "Finest grain; PATCHed repeatedly within its period" },
{ "id": "journal-weekly", "pattern": "^journal/weekly/\\d{4}-W\\d{2}\\.md$", "method": "PUT", "trigger": "First substantive session of a new ISO week (opt-in)", "distinct_because": "ISO-week grain" },
{ "id": "journal-monthly", "pattern": "^journal/monthly/\\d{4}-\\d{2}\\.md$", "method": "PUT", "trigger": "First substantive session of a new month", "distinct_because": "Month grain" },
{ "id": "journal-quarterly", "pattern": "^journal/quarterly/\\d{4}-Q[1-4]\\.md$", "method": "PUT", "trigger": "Manual / on request only", "distinct_because": "Strategic grain; never auto-fires" },
{ "id": "journal-annual", "pattern": "^journal/annual/\\d{4}\\.md$", "method": "PUT", "trigger": "Manual / on request only", "distinct_because": "Coarsest grain; never auto-fires" },
{ "id": "journal-templates", "pattern": "^journal/templates/.+\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Holds templates, not journal content" },
{ "id": "projects-active", "pattern": "^projects/active/[^/]+\\.md$", "method": "PUT", "trigger": "Work in motion now", "distinct_because": "Default state for anything being worked", "status": "active" },
{ "id": "projects-incubating", "pattern": "^projects/incubating/[^/]+\\.md$", "method": "PUT", "trigger": "Idea captured, work not started", "distinct_because": "Pre-work", "status": "incubating" },
{ "id": "projects-on-hold", "pattern": "^projects/on-hold/[^/]+\\.md$", "method": "PUT", "trigger": "Paused but still tracked", "distinct_because": "Resumable; not terminal", "status": "on-hold" },
{ "id": "projects-archived", "pattern": "^projects/archived/[^/]+\\.md$", "method": "PUT", "trigger": "Done, abandoned, or rolled up", "distinct_because": "Terminal; kept for history", "status": "archived" },
{ "id": "projects-template", "pattern": "^projects/project-template\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Template, not a project" },
{ "id": "areas", "pattern": "^areas/(business|personal|learning|systems)/[^/]+\\.md$", "method": "PUT", "trigger": "Ongoing domain with no finish line", "distinct_because": "No end state — disqualifies it from projects/" },
{ "id": "resources-people", "pattern": "^resources/people/[^/]+\\.md$", "method": "PUT", "trigger": "A fact about a specific person", "distinct_because": "Keyed to a person" },
{ "id": "resources-companies", "pattern": "^resources/companies/[^/]+\\.md$", "method": "PUT", "trigger": "A fact about an organization", "distinct_because": "Keyed to an organization, not an individual" },
{ "id": "resources-concepts", "pattern": "^resources/concepts/[^/]+\\.md$", "method": "PUT", "trigger": "A reusable concept/idea", "distinct_because": "An idea vs an external source" },
{ "id": "resources-references", "pattern": "^resources/references/[^/]+\\.md$", "method": "PUT", "trigger": "An external source/link worth keeping", "distinct_because": "Points outward" },
{ "id": "resources-meetings", "pattern": "^resources/meetings/\\d{4}-\\d{2}-\\d{2}-[^/]+\\.md$", "method": "PUT", "trigger": "Notes tied to a specific meeting", "distinct_because": "Event-anchored to a meeting" },
{ "id": "decisions-by-date", "pattern": "^decisions/by-date/\\d{4}-\\d{2}-\\d{2}-[^/]+\\.md$", "method": "PUT", "trigger": "A non-obvious decision worth recording", "distinct_because": "Chronological system of record for decisions" },
{ "id": "decisions-template", "pattern": "^decisions/decision-template\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Template, not a decision" },
{ "id": "agent-marker", "pattern": "^_agent/echo-vault\\.md$", "method": "PUT", "trigger": "Bootstrap / schema migration only", "distinct_because": "Plugin-owned probe; never hand-edited" },
{ "id": "agent-context", "pattern": "^_agent/context/[^/]+\\.md$", "method": "PATCH", "trigger": "Active scope changes / task bundles", "distinct_because": "Single live scope pointer + bundles" },
{ "id": "agent-semantic", "pattern": "^_agent/memory/semantic/[^/]+\\.md$", "method": "PUT", "trigger": "A durable fact/pattern (incl. operator-preferences.md)", "distinct_because": "Timeless fact" },
{ "id": "agent-episodic", "pattern": "^_agent/memory/episodic/[^/]+\\.md$", "method": "PUT", "trigger": "A record of what happened, when", "distinct_because": "Anchored to an event in time" },
{ "id": "agent-working", "pattern": "^_agent/memory/working/[^/]+\\.md$", "method": "PUT", "trigger": "Short-lived state for the current effort", "distinct_because": "Explicitly transient" },
{ "id": "agent-sessions", "pattern": "^_agent/sessions/\\d{4}-\\d{2}-\\d{2}(-\\d{4})?-[^/]+\\.md$", "method": "PUT", "trigger": "A substantive session ends", "distinct_because": "Per-session record (new ones require HHMM)" },
{ "id": "agent-health", "pattern": "^_agent/health/\\d{4}-\\d{2}-vault-health\\.md$", "method": "PUT", "trigger": "First substantive session of a month", "distinct_because": "Vault integrity, not work narrative" },
{ "id": "agent-heartbeat", "pattern": "^_agent/heartbeat/[^/]+\\.md$", "method": "PUT", "trigger": "End of every session", "distinct_because": "O(1) orientation pointer; overwritten, never grows" },
{ "id": "agent-templates", "pattern": "^_agent/templates/.+\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Holds templates, not memory" },
{ "id": "agent-skills-active", "pattern": "^_agent/skills/active/[^/]+\\.md$", "method": "PUT", "trigger": "A skill/plugin catalogued as a capability", "distinct_because": "Catalogs a capability vs the build effort" },
{ "id": "agent-skills-archived","pattern": "^_agent/skills/archived/[^/]+\\.md$", "method": "PUT", "trigger": "A catalogued skill is retired", "distinct_because": "Terminal state of the skill catalog" },
{ "id": "agent-locks", "pattern": "^_agent/locks/[^/]+\\.lock$", "method": "PUT", "trigger": "Advisory multi-writer lock acquire/release", "distinct_because": "Concurrency coordination, not memory" },
{ "id": "leaf-readme", "pattern": "^(.+/)?README\\.md$", "method": "PUT", "trigger": "Bootstrap leaf signpost / vault root README", "distinct_because": "Human signpost, not read for routing" }
],
"retired": [
{ "pattern": "^reviews/", "retired_in_schema": 2, "replacement": "journal/{weekly,monthly,quarterly,annual}/ and _agent/health/" },
{ "pattern": "^decisions/by-project/", "retired_in_schema": 1, "replacement": "[[wikilink]] under the project's ## Key Decisions" },
{ "pattern": "^archive/", "retired_in_schema": 0, "replacement": "projects/archived/ and _agent/skills/archived/" },
{ "pattern": "^(CLAUDE|BOOTSTRAP|STRUCTURE|index)\\.md$", "retired_in_schema": 1, "replacement": "All control logic lives in the plugin references/, not the vault" }
]
}
PK
¥Ó\Xäù^‡^‡skills/echo-memory/SKILL.md---
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 — hardcoded for this personal plugin:
```
OBSIDIAN_BASE = https://echoapi.alwisp.com
OBSIDIAN_KEY = 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab
```
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/`:
- `scripts/echo.sh` — the **validated API client**; prefer it over hand-built `curl` (below)
- `scripts/routing.json` — the **canonical, machine-readable** route manifest (the routing map's source of truth; the linter enforces it)
- `scripts/vault-lint.sh` — read-only invariant checker (Vault Health)
- `scripts/bootstrap.sh` / `scripts/migrate.sh` — deterministic vault setup/repair and schema migration
## Bundled Tooling (prefer over raw curl)
All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/`.
**`scripts/echo.sh` — 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, idempotent append, correct `::` heading targets, and frontmatter patches. The raw `curl` recipes later in this file are the underlying mechanics / fallback — reach for them only if `echo.sh` 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.sh"
"$ECHO" get <path> # 404 -> exit 44
"$ECHO" ls <dir> ; "$ECHO" map <path> # listing / document-map
"$ECHO" search <terms...>
"$ECHO" put <path> <file> # create/overwrite (read-back verified)
"$ECHO" append <path> "<line>" # idempotent: skips if the exact line exists
"$ECHO" patch <path> append heading "<H1::Sub>" <file>
"$ECHO" fm <path> updated '"2026-06-19"' ; "$ECHO" bump <path> # frontmatter
"$ECHO" lock <session-id> ; "$ECHO" unlock <session-id>
```
**Bootstrap / migrate** are scripts now, not hand-run curl loops: `scripts/bootstrap.sh [--dry-run]` (idempotent, probe-before-write, never overwrites) and `scripts/migrate.sh [--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
"$ECHO" lock "cc-$(date +%s)" # exit 75 if another session holds a fresh lock
# ... do the writes ...
"$ECHO" unlock "cc-$(date +%s)"
```
The lock is cooperative (a stale lock past `ECHO_LOCK_TTL`, default 15 min, is reclaimable) and 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), `/echo-triage` (drain the inbox), `/echo-health` (run the linter). 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
The cold-start reads are independent — **issue them in parallel** (one batch of 5–6 GETs), not sequentially. Parallel loading is ~3× faster wall-clock for the same call count.
| # | 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.** Compare `## Scope` in `current-context.md` (GET #3) against what Jason just asked for. If they diverge, follow **Scope Switching** to record the prior scope and set the new one.
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 project is in play**, follow up with a **search across all lifecycle subfolders** (`active/`, `incubating/`, `on-hold/`, `archived/`) — searching one folder at a time misses notes filed elsewhere. Search by **both the slug AND any human title** Jason used in this conversation:
```bash
# slug
curl -s -X POST -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/search/simple/?query=<slug>"
# human title (avoids missing notes filed under a different name)
curl -s -X POST -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/search/simple/?query=<human+title>"
```
Then read whichever match lives at `projects/<lifecycle>/<slug>.md`.
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`.
### Before you write — search first (MANDATORY for new notes)
**Before creating any new note at `projects/<lifecycle>/<slug>.md`, `_agent/memory/semantic/<slug>.md`, `resources/people/<name>.md`, or any other slug-addressed location, search the whole vault for that 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
curl -s -X POST \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/search/simple/?query=<slug>"
```
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)
POST appends to the end of a file. It is **not idempotent** — running the same write twice (network retry, replay, re-trigger) produces duplicate lines that grow files silently. Before any POST that adds an entry to:
- `inbox/captures/inbox.md`
- a daily note's `## Agent Log` section
- a `## Fact / Pattern` / `## Observations` / `## Log` heading
…GET the target file (or the heading, via `/heading/...`) and substring-search for the exact line you're about to write. If present, skip the POST. The extra GET is cheap and pays for itself within a few sessions.
This rule does **not** apply to PUT (which is fully replacing the file) or PATCH `replace` (which is overwriting a section by design).
### Append to a file (default — additive entries)
Write content to a temp file first to handle multi-line markdown cleanly, then POST:
```bash
cat > /tmp/obs_entry.md << 'OBSEOF'
- 2026-06-05: <your entry here>
OBSEOF
curl -s -X POST \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_entry.md \
"https://echoapi.alwisp.com/vault/inbox/captures/inbox.md"
```
POST appends to the end of a file (creating it if absent). Use it for inbox captures and log sections.
### Patch a specific heading (targeted update)
**Heading targets use the FULL heading path, `::`-delimited from the top-level heading** — a bare subheading name fails with `400 invalid-target` and the write is lost. For example, `## Fact / Pattern` under the `# Operator Preferences` H1 is targeted as `Operator Preferences::Fact / Pattern`.
**Default: GET the document map first** (every first PATCH to a file in a session — cache the result mentally for subsequent PATCHes to the same file). This eliminates the most common failure mode of PATCH:
```bash
curl -s -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Accept: application/vnd.olrapi.document-map+json" \
"https://echoapi.alwisp.com/vault/_agent/memory/semantic/operator-preferences.md"
```
Returns `{ "headings": [...], "blocks": [...], "frontmatterFields": [...] }`. Copy the heading string verbatim into `Target`. Only skip the doc-map GET if you wrote the file yourself in this session (you already know its structure).
Then PATCH:
```bash
cat > /tmp/obs_patch.md << 'OBSEOF'
Jason prefers status updates that lead with the decision.
OBSEOF
curl -s -X PATCH \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Operation: append" \
-H "Target-Type: heading" \
-H "Target: Operator Preferences::Fact / Pattern" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_patch.md \
"https://echoapi.alwisp.com/vault/_agent/memory/semantic/operator-preferences.md"
```
Use `Operation: replace` to overwrite a section entirely (e.g. a project's `Project Name::Status`). Percent-encode non-ASCII characters in the `Target` header; spaces are fine.
### Bump `updated:` after meaningful changes
When a PATCH or PUT changes meaningful content (status update, decision recorded, current-status replacement, scope switch), also PATCH the frontmatter `updated:` field to today's date. This keeps stale-detection queries honest.
```bash
curl -s -X PATCH \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Operation: replace" -H "Target-Type: frontmatter" -H "Target: updated" \
-H "Content-Type: application/json" --data '"2026-06-06"' \
"https://echoapi.alwisp.com/vault/projects/active/<slug>.md"
```
Skip the bump for **routine log appends** — adding an Agent Log line, an inbox capture, or a timestamped Observations bullet doesn't constitute a meaningful content change. Bump on substance, not on heartbeat.
### Create or overwrite a file (PUT)
```bash
cat > /tmp/obs_file.md << 'OBSEOF'
---
type: project
status: active
created: 2026-06-05
updated: 2026-06-05
tags: []
agent_written: true
source_notes: []
---
# Project Name
## Status
...
## Related
- [[journal/daily/2026-06-05]]
OBSEOF
curl -s -X PUT \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_file.md \
"https://echoapi.alwisp.com/vault/projects/active/my-project.md"
```
The API creates intermediate directories automatically.
### Search the vault
```bash
curl -s -X POST \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
"https://echoapi.alwisp.com/search/simple/?query=your+search+terms"
```
## 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).
When scope changes:
1. PATCH `prepend` a dated bullet to `## Scope History` capturing the **prior** scope (one line: `- 2026-06-06: <prior scope summary>`). If `## Scope History` doesn't exist yet, POST the heading first, same pattern as the daily-note Agent Log.
2. PATCH `replace` `## Scope` with the new scope.
3. PATCH the frontmatter `updated:` field.
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.
## 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. Detect the weekly trigger with `date +%G-W%V` and check whether that week's note already exists; monthly with `date +%Y-%m`.
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> "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/vault-lint.sh"
```
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`.
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.
## 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):**
1. Try to GET `journal/daily/YYYY-MM-DD.md`.
2. If 404 — PUT a fresh daily note from the template, then PATCH.
3. If 200 but `## Agent Log` is missing — POST `\n\n## Agent Log\n` to the file to add the heading, then PATCH. **Detect the heading by grepping the raw markdown for an anchored `^## Agent Log` line — do NOT grep the document-map JSON, whose headings are full `::`-delimited paths (e.g. `"2026-06-07::Agent Log"`); a bare `"Agent Log"` match fails there and a duplicate heading gets appended.**
4. PATCH-append the entry under the target `<YYYY-MM-DD>::Agent Log` (the H1 of a daily note is the date, so that's the full target path).
```bash
DATE=$(date +%Y-%m-%d)
DAILY="https://echoapi.alwisp.com/vault/journal/daily/${DATE}.md"
AUTH="Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab"
# 1+2: ensure the daily note exists (PUT from template if missing)
HTTP=$(curl -s -o /dev/null -w "%{http_code}" -H "$AUTH" "$DAILY")
if [ "$HTTP" = "404" ]; then
curl -s -H "$AUTH" "https://echoapi.alwisp.com/vault/journal/templates/daily-note-template.md" \
| sed "s/{{date:YYYY-MM-DD}}/${DATE}/g" \
| curl -s -X PUT -H "$AUTH" -H "Content-Type: text/markdown" --data-binary @- "$DAILY"
fi
# 3: ensure the `## Agent Log` heading exists.
# Grep the RAW markdown for an anchored heading line. Do NOT grep the document-map
# JSON: its headings are full "::"-delimited paths (e.g. "2026-06-07::Agent Log"), so a
# bare '"Agent Log"' match never matches and a DUPLICATE heading gets appended each time.
if ! curl -s -H "$AUTH" "$DAILY" | grep -q '^## Agent Log'; then
printf '\n\n## Agent Log\n' | curl -s -X POST -H "$AUTH" -H "Content-Type: text/markdown" --data-binary @- "$DAILY"
fi
# 4: PATCH-append the entry
cat > /tmp/agent_log.md << OBSEOF
- ${DATE}: <one-line entry here>
OBSEOF
curl -s -X PATCH -H "$AUTH" \
-H "Operation: append" \
-H "Target-Type: heading" \
-H "Target: ${DATE}::Agent Log" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/agent_log.md \
"$DAILY"
```
## 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.
See `references/session-log-template.md` for the body format.
```bash
cat > /tmp/obs_session.md << 'OBSEOF'
<session log content — see references/session-log-template.md>
OBSEOF
curl -s -X PUT \
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_session.md \
"https://echoapi.alwisp.com/vault/_agent/sessions/2026-06-05-1430-my-session.md"
```
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 GET (this is what closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read):
```bash
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf '%s @ %s\n' "_agent/sessions/${SESSION_FILENAME}" "$NOW" \
| curl -s -X PUT -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
-H "Content-Type: text/markdown" --data-binary @- \
"https://echoapi.alwisp.com/vault/_agent/heartbeat/last-session.md"
```
`last-session.md` is a single-line pointer 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.
PK
<¡Ó\skills/echo-memory/scaffold/PK
<¡Ó\;ˆ)skills/echo-memory/scaffold/echo-vault.md---
type: reference
status: active
created: {{DATE}}
updated: {{DATE}}
tags: [agent, system, marker]
agent_written: true
source_notes: []
schema_version: 2
bootstrapped: {{DATE}}
managed_by: echo-memory-plugin
---
# ECHO Vault Marker
This file marks the vault as bootstrapped by the **echo-memory plugin** and records its schema version. The plugin's loading procedure GETs this file as its "is this vault set up?" probe; a `404` triggers a fresh bootstrap.
- `schema_version` — bumped by the plugin when the vault layout changes; a mismatch is the hook for a migration pass (see `references/bootstrap.md`).
- Do not hand-edit. The plugin owns this file.
PK
<¡Ó\&skills/echo-memory/scaffold/templates/PK
<¡Ó\.skills/echo-memory/scaffold/templates/journal/PK
<¡Ó\8skills/echo-memory/scaffold/templates/journal/templates/PK
<¡Ó\ù m22Qskills/echo-memory/scaffold/templates/journal/templates/weekly-review-template.md---
type: review
status: active
created: {{date:gggg-[W]WW}}
updated: {{date:gggg-[W]WW}}
tags: [review, weekly]
agent_written: true
source_notes: []
period: weekly
---
# Weekly Review
## Completed Work
## Open Loops
## Stale Projects
## Decisions
## Priorities Next Week
## Agent Notes
## Related
PK
<¡Ó\½#lC**Nskills/echo-memory/scaffold/templates/journal/templates/daily-note-template.md---
type: daily-note
status: active
created: {{date:YYYY-MM-DD}}
updated: {{date:YYYY-MM-DD}}
tags: [daily, journal]
agent_written: false
source_notes: []
---
# {{date:YYYY-MM-DD}}
## Top Priorities
## Schedule / Commitments
## Open Loops
## Notes
## Context for AI
## Agent Log
## Related
PK
<¡Ó\-skills/echo-memory/scaffold/templates/_agent/PK
<¡Ó\7skills/echo-memory/scaffold/templates/_agent/templates/PK
<¡Ó\±¸A11Qskills/echo-memory/scaffold/templates/_agent/templates/working-memory-template.md---
type: working-memory
status: active
created: {{date:YYYY-MM-DDTHH:mm}}
updated: {{date:YYYY-MM-DDTHH:mm}}
tags: [agent, working-memory]
agent_written: true
source_notes: []
expires_after: 48h
---
# Working Context
## Active Focus
## Recent Decisions
## Open Threads
## Relevant Links
## Related
PK
<¡Ó\ •ãWWRskills/echo-memory/scaffold/templates/_agent/templates/semantic-memory-template.md---
type: semantic-memory
status: active
created: {{date:YYYY-MM-DD}}
updated: {{date:YYYY-MM-DD}}
tags: [agent, semantic-memory]
agent_written: true
source_notes: []
confidence: high
last_reviewed: {{date:YYYY-MM-DD}}
---
# Semantic Memory Title
## Fact / Pattern
## Evidence
## Recommendation or Implication
## Review Notes
## Related
PK
<¡Ó\üĉ„xxNskills/echo-memory/scaffold/templates/_agent/templates/session-log-template.md---
type: session-log
status: complete
created: {{date:YYYY-MM-DDTHH:mm}}
updated: {{date:YYYY-MM-DDTHH:mm}}
tags: [agent, session]
agent_written: true
source_notes: []
session_date: {{date:YYYY-MM-DD}}
client: claude-code
---
# Session Log
## Goal
## Notes Read
## Actions Taken
## Decisions Made
## Outputs Created
## Open Threads
## Suggested Next Step
## Related
PK
<¡Ó\\(iUUQskills/echo-memory/scaffold/templates/_agent/templates/context-bundle-template.md---
type: context-bundle
status: active
created: {{date:YYYY-MM-DD}}
updated: {{date:YYYY-MM-DD}}
tags: [agent, context]
agent_written: true
source_notes: []
scope:
refresh_strategy: on-demand
---
# Context Bundle Title
## Scope
## Active Priorities
## Relevant Entities
## Key Decisions
## Open Questions
## Source Notes
## Related
PK
<¡Ó\/skills/echo-memory/scaffold/templates/projects/PK
<¡Ó\QYb\IIBskills/echo-memory/scaffold/templates/projects/project-template.md---
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
## Goals
## Current Context
## Open Loops
## Key Decisions
## Related Notes
## Session History
## Related
PK
<¡Ó\0skills/echo-memory/scaffold/templates/decisions/PK
<¡Ó\Ád°..Dskills/echo-memory/scaffold/templates/decisions/decision-template.md---
type: decision
status: complete
created: {{date:YYYY-MM-DD}}
updated: {{date:YYYY-MM-DD}}
tags: [decision]
agent_written: true
source_notes: []
decision_date: {{date:YYYY-MM-DD}}
impact: medium
---
# Decision Title
## Context
## Decision
## Rationale
## Consequences
## Follow-Up
## Related
PK
<¡Ó\R"hh+skills/echo-memory/scaffold/README.vault.md# ECHO Memory Vault
This Obsidian vault is the persistent memory substrate ("second brain") for its operator. It is read and written across Claude / CoWork sessions through the Obsidian Local REST API.
**This vault holds data, not logic.** All operating procedure — how the vault is bootstrapped, how notes are routed, the taxonomy, frontmatter conventions, and safety rules — lives in the **`echo-memory` plugin**, which is the single source of truth. There are intentionally no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` control docs in this vault; updating or porting ECHO means updating or installing the plugin, not editing files here.
- **Layout:** see the plugin's `references/vault-layout.md`.
- **Operating contract & safety:** see the plugin's `references/operating-contract.md`.
- **Bootstrap / repair:** see the plugin's `references/bootstrap.md`.
- **Version marker:** `_agent/echo-vault.md` records the schema version and bootstrap date.
Folders: `inbox/`, `journal/` (daily + weekly/monthly/quarterly/annual rollups), `projects/`, `areas/`, `resources/`, `decisions/`, and the agent subtree `_agent/`.
PK
<¡Ó\$skills/echo-memory/scaffold/anchors/PK
<¡Ó\+ÙÝyÌÌ;skills/echo-memory/scaffold/anchors/current-context.seed.md---
type: context-bundle
status: active
created: {{DATE}}
updated: {{DATE}}
tags: [agent, context]
agent_written: true
source_notes: []
scope:
refresh_strategy: on-demand
---
# Current Context
## Scope
<!-- The single active scope. Replaced (PATCH replace) on each scope switch. -->
## Scope History
<!-- Dated bullets of prior scopes, newest first: `- {{DATE}}: <prior scope summary>`. Trim to ~10. -->
## Active Priorities
## Open Questions
## Related
PK
<¡Ó\\õÓ  1skills/echo-memory/scaffold/anchors/inbox.seed.md# Inbox — Captures
Quick captures land here as date-prefixed lines (`- {{DATE}}: <thing>`), one per line, via POST. Triage routes durable items to their proper home (see the echo-memory skill's Inbox Triage). Don't delete originals on triage — the processing log is the audit trail.
PK
<¡Ó\[ COèè@skills/echo-memory/scaffold/anchors/operator-preferences.seed.md---
type: semantic-memory
status: active
created: {{DATE}}
updated: {{DATE}}
tags: [agent, semantic-memory, operator]
agent_written: true
source_notes: []
confidence: low
last_reviewed: {{DATE}}
---
# Operator Preferences
## Operator
<!-- One paragraph: who the operator is. Leave for the operator to confirm; do not fabricate. -->
## Fact / Pattern
<!-- Promoted, deduped, timeless rules. No date prefix. Add only when a rule is stable. -->
## Observations
<!-- Timestamped raw observations. Default landing zone for new evidence: `- {{DATE}}: ...` -->
## Evidence
## Recommendation or Implication
## Review Notes
Seeded empty at bootstrap ({{DATE}}). Raise confidence once preferences are confirmed through day-to-day use.
## Related
PK
!©Ó\kÝžÜww ¤README.mdPK
ĤÓ\íAž.claude-plugin/PK
ĤÓ\²WÒD¤Ë.claude-plugin/plugin.jsonPK
Œ¤Ó\ íAcommands/PK
¤Ó\Ìsõ’èè¤<commands/echo-load.mdPK
Œ¤Ó\%â¨HTT¤Wcommands/echo-health.mdPK
‰¤Ó\-YO>KK¤à!commands/echo-triage.mdPK
†¤Ó\
µ®ll¤`%commands/echo-save.mdPK
<¡Ó\íAÿ)skills/PK
¥Ó\íA$*skills/echo-memory/PK
½¤Ó\íAU*skills/echo-memory/references/PK
²¤Ó\«Á}¤+¤+,¤*skills/echo-memory/references/routing-map.mdPK
<¡Ó\bCIÒ›!!-¤Vskills/echo-memory/references/vault-layout.mdPK
<¡Ó\É9väY Y 5¤exskills/echo-memory/references/session-log-template.mdPK
¯¤Ó\å,ò^%%*¤skills/echo-memory/references/bootstrap.mdPK
½¤Ó\TóIº``3¤skills/echo-memory/references/operating-contract.mdPK
·¤Ó\ÿŒhK"K".¤skills/echo-memory/references/api-reference.mdPK
q¤Ó\íA¾Üskills/echo-memory/scripts/PK
ò£Ó\% O„²²%í÷Üskills/echo-memory/scripts/migrate.shPK
,¤Ó\µ—ëü!ü!"íììskills/echo-memory/scripts/echo.shPK
æ£Ó\ßNNjj'í(skills/echo-memory/scripts/bootstrap.shPK
q¤Ó\О~öº-º-(í× skills/echo-memory/scripts/vault-lint.shPK
”£Ó\Á)ýp&p&'¤×Nskills/echo-memory/scripts/routing.jsonPK
¥Ó\Xäù^‡^‡¤Œuskills/echo-memory/SKILL.mdPK
<¡Ó\íA#ýskills/echo-memory/scaffold/PK
<¡Ó\;ˆ)¤skills/echo-memory/scaffold/echo-vault.mdPK
<¡Ó\&íA8skills/echo-memory/scaffold/templates/PK
<¡Ó\.íA|skills/echo-memory/scaffold/templates/journal/PK
<¡Ó\8íAÈskills/echo-memory/scaffold/templates/journal/templates/PK
<¡Ó\ù m22Q¤skills/echo-memory/scaffold/templates/journal/templates/weekly-review-template.mdPK
<¡Ó\½#lC**N¤¿skills/echo-memory/scaffold/templates/journal/templates/daily-note-template.mdPK
<¡Ó\-íAUskills/echo-memory/scaffold/templates/_agent/PK
<¡Ó\7íA skills/echo-memory/scaffold/templates/_agent/templates/PK
<¡Ó\±¸A11Q¤õskills/echo-memory/scaffold/templates/_agent/templates/working-memory-template.mdPK
<¡Ó\ •ãWWR¤skills/echo-memory/scaffold/templates/_agent/templates/semantic-memory-template.mdPK
<¡Ó\üĉ„xxN¤\skills/echo-memory/scaffold/templates/_agent/templates/session-log-template.mdPK
<¡Ó\\(iUUQ¤@
skills/echo-memory/scaffold/templates/_agent/templates/context-bundle-template.mdPK
<¡Ó\/íA skills/echo-memory/scaffold/templates/projects/PK
<¡Ó\QYb\IIB¤Q skills/echo-memory/scaffold/templates/projects/project-template.mdPK
<¡Ó\0íAú
skills/echo-memory/scaffold/templates/decisions/PK
<¡Ó\Ád°..D¤Hskills/echo-memory/scaffold/templates/decisions/decision-template.mdPK
<¡Ó\R"hh+¤Øskills/echo-memory/scaffold/README.vault.mdPK
<¡Ó\$íA‰skills/echo-memory/scaffold/anchors/PK