ver 0.9
This commit is contained in:
@@ -30,48 +30,70 @@ The endpoint has a **valid TLS certificate**, so `-k` is not needed (add it only
|
||||
- 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/`:
|
||||
Executable logic ships under `scripts/` — **pure Python**, so the whole toolchain runs identically on Windows, macOS, and Linux (only a Python 3 interpreter is required; no bash, no platform-specific `date`):
|
||||
|
||||
- `scripts/echo.sh` — the **validated API client**; prefer it over hand-built `curl` (below)
|
||||
- `scripts/echo.py` — the **validated API client**; prefer it over hand-built `curl` (the *nix fallback in `references/api-reference.md`)
|
||||
- `scripts/routing.json` — the **canonical, machine-readable** route manifest (the routing map's source of truth; the linter enforces it)
|
||||
- `scripts/vault-lint.sh` — read-only invariant checker (Vault Health)
|
||||
- `scripts/bootstrap.sh` / `scripts/migrate.sh` — deterministic vault setup/repair and schema migration
|
||||
- `scripts/vault_lint.py` — read-only invariant checker (Vault Health)
|
||||
- `scripts/check_routing.py` — verifies the routing docs stay in sync with `routing.json` (dev/CI; offline)
|
||||
- `scripts/bootstrap.py` / `scripts/migrate.py` — deterministic vault setup/repair and schema migration
|
||||
|
||||
## Bundled Tooling (prefer over raw curl)
|
||||
|
||||
All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/`.
|
||||
All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/`. **Invoke with `python3`** (on Windows where that isn't on PATH, use `python` or `py -3`).
|
||||
|
||||
**`scripts/echo.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).
|
||||
**`scripts/echo.py` — use this for every read/write.** It centralizes auth, **HTTP-status checking** (a failed write exits non-zero instead of looking like success), one bounded retry on 5xx/connection errors, whole-line idempotent append, correct `::` heading targets, frontmatter patches, a read-back-confirmed advisory lock, and a one-call cold-start `load`. The raw `curl` recipes in `references/api-reference.md` are the underlying mechanics / *nix fallback — reach for them only if `echo.py` is unavailable, and if you do, **check the HTTP status yourself** (the PATCH-heading `400 invalid-target` failure silently loses writes otherwise).
|
||||
|
||||
```bash
|
||||
ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.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>
|
||||
ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" # call as: python3 "$ECHO" <cmd> ...
|
||||
python3 "$ECHO" load # cold-start: the 6 orientation reads in one call
|
||||
python3 "$ECHO" get <path> # 404 -> exit 44
|
||||
python3 "$ECHO" ls <dir> ; python3 "$ECHO" map <path> # listing / document-map
|
||||
python3 "$ECHO" search <terms...>
|
||||
python3 "$ECHO" put <path> <file> # create/overwrite (read-back verified)
|
||||
python3 "$ECHO" append <path> "<line>" # idempotent: skips only on an exact whole-line match
|
||||
python3 "$ECHO" patch <path> append heading "<H1::Sub>" <file>
|
||||
python3 "$ECHO" fm <path> updated '"2026-06-19"' ; python3 "$ECHO" bump <path> # frontmatter
|
||||
python3 "$ECHO" scope show ; python3 "$ECHO" scope set "<text>"
|
||||
python3 "$ECHO" lock <session-id> ; python3 "$ECHO" unlock <session-id>
|
||||
```
|
||||
|
||||
**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`.
|
||||
### High-level memory ops (prefer these — they do the routing for you)
|
||||
|
||||
These collapse the multi-step write/recall discipline into one call each, backed by the **entity index** (`_agent/index/entities.json`, a slug→{path,kind,title,aliases} registry). **Default to them** — they are how this skill reduces the input needed to route and cross-link memory:
|
||||
|
||||
```bash
|
||||
python3 "$ECHO" capture "<title>" <bodyfile> --kind <kind> [--aliases a,b] [--source p1,p2] [--status s] [--date YYYY-MM-DD] [--domain business]
|
||||
python3 "$ECHO" capture "<title>" --inbox # unknown home -> idempotent inbox line
|
||||
python3 "$ECHO" resolve "<mention>" # mention -> canonical path (or where to create)
|
||||
python3 "$ECHO" recall "<query>" # search + 1-hop link/source expansion -> connected context
|
||||
python3 "$ECHO" link <pathA> <pathB> # add reciprocal [[Related]] links A<->B
|
||||
```
|
||||
|
||||
- **`capture` is the default write.** Given a title + `--kind`, it: resolves the entity via the index (create-vs-update, no manual search-first), derives the canonical path, stamps canonical frontmatter (`type/created/updated/agent_written/source_notes`), updates the index, **auto-links** any known entity mentioned in the body (bidirectionally), and appends today's Agent Log line — one call instead of search→put→bump→log. `--kind` ∈ `person, company, concept, reference, meeting, project, area, semantic, episodic, working, skill, decision`. Use `--inbox` (or omit `--kind`) only when the home is genuinely unknown.
|
||||
- **`resolve` before constructing any path by hand** — it returns the canonical note (matching aliases, so *Bob/Robert/RS* converge) or tells you where a new one goes. This replaces the two-search "search-first" dance for slug-addressed notes.
|
||||
- **`recall` for "what do we know about X"** — it returns the matching notes *and* their one-hop neighbourhood (Related links, `source_notes`), so recall surfaces the web around a topic, not an isolated note.
|
||||
- **`link`** when two existing notes are related but not yet connected; `capture` already auto-links what it detects.
|
||||
|
||||
The index is maintained automatically by `capture`; `sweep.py` rebuilds it and back-fills links for an existing vault (see **Vault Maintenance**).
|
||||
|
||||
**Bootstrap / migrate** are scripts, not hand-run curl loops: `python3 scripts/bootstrap.py [--dry-run]` (idempotent, probe-before-write, never overwrites) and `python3 scripts/migrate.py [--apply]` (reads the marker's `schema_version` and applies migrations; dry-run by default). See `references/bootstrap.md`.
|
||||
|
||||
### Concurrency — the vault is shared, so coordinate writes
|
||||
|
||||
ECHO is read/written by multiple clients (Claude Code **and** CoWork sessions). The single-line files (`heartbeat/last-session.md`, `current-context.md::Scope`, `inbox.md`) assume a single writer at a time. Before a burst of writes in a session that may overlap another, take the **advisory lock**, and release it at session end:
|
||||
|
||||
```bash
|
||||
"$ECHO" lock "cc-$(date +%s)" # exit 75 if another session holds a fresh lock
|
||||
python3 "$ECHO" lock "cc-<session-id>" # exit 75 if another session holds a fresh lock, or you lost the race
|
||||
# ... do the writes ...
|
||||
"$ECHO" unlock "cc-$(date +%s)"
|
||||
python3 "$ECHO" unlock "cc-<session-id>"
|
||||
```
|
||||
|
||||
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.
|
||||
Use a stable `<session-id>` for both calls (any unique string for this session). The lock is read-back-confirmed (after PUT, `echo.py` re-reads and backs off if another writer won the race) and cooperative (a stale lock past `ECHO_LOCK_TTL`, default 15 min, is reclaimable); it lives at `_agent/locks/vault.lock`. It is a courtesy, not a hard mutex — if you can't take it, tell Jason another session may be active rather than racing it.
|
||||
|
||||
### Slash commands
|
||||
|
||||
`/echo-load` (cold-start read), `/echo-save <text>` (route + persist), `/echo-triage` (drain the inbox), `/echo-health` (run the linter). These are explicit entry points to the procedures below.
|
||||
`/echo-load` (cold-start read), `/echo-save <text>` (route + persist via `capture`), `/echo-recall <query>` (recall a topic's neighbourhood), `/echo-triage` (drain the inbox), `/echo-health` (run the linter), `/echo-sweep` (bring the vault up to current spec). These are explicit entry points to the procedures below.
|
||||
|
||||
## Operating Contract & Safety
|
||||
|
||||
@@ -90,7 +112,7 @@ Load at the start of any substantive conversation — anything beyond a single q
|
||||
|
||||
### 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.
|
||||
**Run `python3 "$ECHO" load`** — one call that performs the six orientation reads below and prints each labeled section (404s on today's note / inbox show as absent, not errors; an absent marker is flagged). This replaces issuing six separate GETs by hand. The table documents what `load` reads and why:
|
||||
|
||||
| # | GET | Notes |
|
||||
|---|-----|-------|
|
||||
@@ -106,23 +128,17 @@ Do not read every session log — older sessions are reachable via `POST /search
|
||||
**Reconcile at load (do this every cold start, after the batch returns).** The batch already fetched everything needed for a cheap self-check — run it before diving into the work so memory maintains itself instead of drifting:
|
||||
|
||||
1. **Inbox depth (Inbox Triage).** If `inbox/captures/inbox.md` (GET #6) holds dated capture lines older than ~7 days that were never routed, surface the count once and offer to triage — see **Inbox Triage** below. This is the load-time trigger that makes triage self-firing rather than something you only run when asked.
|
||||
2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — Jason runs several sessions a day across different topics, so the recorded `## Scope` is frequently stale at load. **Silently working under a stale scope is the default failure mode.** To prevent it, at load read the active scope and its freshness in one call — `echo.sh scope show` (prints `## Scope`, `scope_updated`, and how many sessions have been logged since) — and form a one-line judgment: *does this session's request match the recorded scope?* If it diverges, switch **before** doing the work via `echo.sh scope set "<new scope>"` (see **Scope Switching**). If `scope show` reports several sessions logged since the last switch, treat the recorded scope as suspect and confirm with Jason rather than trusting it.
|
||||
2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — Jason runs several sessions a day across different topics, so the recorded `## Scope` is frequently stale at load. **Silently working under a stale scope is the default failure mode.** To prevent it, at load read the active scope and its freshness in one call — `python3 "$ECHO" scope show` (prints `## Scope`, `scope_updated`, and how many sessions have been logged since) — and form a one-line judgment: *does this session's request match the recorded scope?* If it diverges, switch **before** doing the work via `python3 "$ECHO" scope set "<new scope>"` (see **Scope Switching**). If `scope show` reports several sessions logged since the last switch, treat the recorded scope as suspect and confirm with Jason rather than trusting it.
|
||||
|
||||
Keep the reconcile to a single short line to Jason (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.
|
||||
|
||||
**If a specific 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:
|
||||
**If a specific topic/project/person is in play**, pull its connected context in one call:
|
||||
|
||||
```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>"
|
||||
python3 "$ECHO" recall "<topic or title>" # the matching notes AND their one-hop neighbourhood
|
||||
```
|
||||
|
||||
Then read whichever match lives at `projects/<lifecycle>/<slug>.md`.
|
||||
`recall` resolves the term against the entity index, searches, and expands one hop along `## Related` links and `source_notes` — so you get the project note *plus* its linked decisions, people, and prior sessions, not an isolated file. (If you only need the canonical path, `resolve "<title>"` is the O(1) lookup.)
|
||||
|
||||
Do NOT narrate this loading. Reading memory is expected behavior.
|
||||
|
||||
@@ -169,14 +185,15 @@ Write when Jason:
|
||||
|
||||
Write in third person about Jason. Every note carries the canonical frontmatter (see below). Agent-generated notes set `agent_written: true`.
|
||||
|
||||
**Default write path: `capture`.** For a routed memory (a fact about a person/company/project/concept/etc.), prefer `python3 "$ECHO" capture "<title>" <bodyfile> --kind <kind>` — it does the search-first, path derivation, frontmatter, indexing, auto-linking, and Agent-Log line in one call (see **High-level memory ops**). Drop to the lower-level `put`/`patch`/`append` verbs below only for shapes `capture` doesn't model (a project `## Status` replacement, an ADR mirror, a daily-note edit) or to inspect/repair.
|
||||
|
||||
### Before you write — search first (MANDATORY for new notes)
|
||||
|
||||
**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.
|
||||
`capture` and `resolve` enforce this automatically via the entity index. If you write a slug-addressed note **by hand**, first **`resolve` the title/slug** (it matches aliases and returns the canonical path or confirms none exists); if `resolve` is unavailable, `search` the whole vault for the slug. Listing a single folder (e.g. `projects/active/`) is NOT sufficient — a note with the same slug may exist in `projects/on-hold/`, `projects/incubating/`, `projects/archived/`, or under a different folder entirely.
|
||||
|
||||
```bash
|
||||
curl -s -X POST \
|
||||
-H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \
|
||||
"https://echoapi.alwisp.com/search/simple/?query=<slug>"
|
||||
python3 "$ECHO" resolve "<title>" # preferred: alias-aware, O(1)
|
||||
python3 "$ECHO" search <slug> # fallback when the index isn't trusted
|
||||
```
|
||||
|
||||
If a match is found:
|
||||
@@ -190,120 +207,35 @@ Only after the search comes back empty (or you've decided to merge) is it safe t
|
||||
|
||||
### 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:
|
||||
`echo.py append` is **whole-line idempotent**: it GETs the file and skips the POST when the exact line already exists, so a retry, replay, or overlapping session can't grow duplicate lines. Use it for single-line additions to `inbox/captures/inbox.md`, a daily note's `## Agent Log`, or a `## Fact / Pattern` / `## Observations` / `## Log` heading. (A raw POST is **not** idempotent — if you must use curl, GET first and whole-line-match the entry yourself; see `references/api-reference.md`.) This does **not** apply to PUT or PATCH `replace`, which overwrite by design.
|
||||
|
||||
- `inbox/captures/inbox.md`
|
||||
- a daily note's `## Agent Log` section
|
||||
- a `## Fact / Pattern` / `## Observations` / `## Log` heading
|
||||
### Append, patch, put — via the client
|
||||
|
||||
…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:
|
||||
Write multi-line bodies to a file first (use the Write tool — cross-platform, no heredoc) and pass the path; or pipe via stdin (`-`).
|
||||
|
||||
```bash
|
||||
cat > /tmp/obs_entry.md << 'OBSEOF'
|
||||
- 2026-06-05: <your entry here>
|
||||
OBSEOF
|
||||
# additive line (idempotent). Anchor the date on the conversation's currentDate.
|
||||
python3 "$ECHO" append inbox/captures/inbox.md "- <currentDate>: <entry>"
|
||||
|
||||
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"
|
||||
# targeted heading append/replace. The heading Target is the FULL `::`-delimited path
|
||||
# from the H1 — a bare subheading returns 400 invalid-target and the write is LOST.
|
||||
# If unsure of the exact path, GET the document map first and copy the heading verbatim:
|
||||
python3 "$ECHO" map _agent/memory/semantic/operator-preferences.md
|
||||
python3 "$ECHO" patch _agent/memory/semantic/operator-preferences.md \
|
||||
append heading "Operator Preferences::Fact / Pattern" <bodyfile>
|
||||
# `replace` (instead of `append`) overwrites a section entirely (e.g. a project's "Name::Status").
|
||||
|
||||
# create/overwrite a whole file (read-back verified; intermediate dirs auto-created)
|
||||
python3 "$ECHO" put projects/active/<slug>.md <bodyfile>
|
||||
|
||||
# frontmatter freshness after a SUBSTANTIVE change (status/decision/scope) — NOT routine log appends
|
||||
ECHO_TODAY=<currentDate> python3 "$ECHO" bump projects/active/<slug>.md # sets updated: to today
|
||||
|
||||
# search (run before creating any slug-addressed note — see the search-first rule above)
|
||||
python3 "$ECHO" search <terms...>
|
||||
```
|
||||
|
||||
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"
|
||||
```
|
||||
Every PUT body needs the canonical YAML frontmatter (see `references/vault-layout.md`); set `agent_written: true` and `source_notes`. Bump `updated:` on substance, not on heartbeat — adding an Agent Log line, an inbox capture, or a timestamped Observations bullet is not a meaningful content change. Raw curl mechanics for every verb live in `references/api-reference.md`.
|
||||
|
||||
## Scope Switching (`current-context.md`)
|
||||
|
||||
@@ -312,16 +244,16 @@ curl -s -X POST \
|
||||
**Preferred — one command:**
|
||||
|
||||
```bash
|
||||
"$ECHO" scope set "<new scope summary>"
|
||||
python3 "$ECHO" scope set "<new scope summary>"
|
||||
```
|
||||
|
||||
`scope set` does the whole switch atomically and correctly: it archives the **prior** scope to `## Scope History` (dated, truncated), replaces `## Scope` with the new text, and stamps the `scope_updated:` frontmatter timestamp. That timestamp is the **freshness signal** — it's what `echo.sh scope show` and the `vault-lint.sh` drift check read to tell whether the recorded scope still reflects current work. Always switch through `scope set` so `scope_updated` stays honest; a hand-edited `## Scope` that skips the stamp reintroduces silent drift.
|
||||
`scope set` does the whole switch atomically and correctly: it archives the **prior** scope to `## Scope History` (dated, truncated), replaces `## Scope` with the new text, and stamps the `scope_updated:` frontmatter timestamp. That timestamp is the **freshness signal** — it's what `echo.py scope show` and the `vault_lint.py` drift check read to tell whether the recorded scope still reflects current work. Always switch through `scope set` so `scope_updated` stays honest; a hand-edited `## Scope` that skips the stamp reintroduces silent drift.
|
||||
|
||||
**Manual fallback** (only if `echo.sh` is unavailable): PATCH `prepend` the prior scope to `## Scope History`, PATCH `replace` `## Scope`, then PATCH the frontmatter `scope_updated:` (and `updated:`) to today. Note `scope_updated` must already exist in frontmatter — a `PATCH replace` on a missing field returns `400 invalid-target`; run `bootstrap.sh` repair to add it.
|
||||
**Manual fallback** (only if `echo.py` is unavailable): PATCH `prepend` the prior scope to `## Scope History`, PATCH `replace` `## Scope`, then PATCH the frontmatter `scope_updated:` (and `updated:`) to today (see `references/api-reference.md` for the raw curl). Note `scope_updated` must already exist in frontmatter — a `PATCH replace` on a missing field returns `400 invalid-target`; run `bootstrap.py` to add it.
|
||||
|
||||
This keeps a rolling trail of recent scopes in one file instead of spawning separate stash notes. Trim Scope History to the last ~10 entries when it grows past that.
|
||||
|
||||
**Drift backstop:** `vault-lint.sh` flags when ≥ `SCOPE_STALE_SESSIONS` (default 3) session logs are dated after `scope_updated` — i.e. work happened without a scope switch. It's advisory (surfaced in Vault Health / `/echo-health`), the mechanical safety net under the load-time judgment above.
|
||||
**Drift backstop:** `vault_lint.py` flags when ≥ `SCOPE_STALE_SESSIONS` (default 3) session logs are dated after `scope_updated` — i.e. work happened without a scope switch. It's advisory (surfaced in Vault Health / `/echo-health`), the mechanical safety net under the load-time judgment above.
|
||||
|
||||
## Journal Rollups (the journal is one continuum)
|
||||
|
||||
@@ -335,7 +267,7 @@ The journal is a single append-only chronological stream. Rollups are just coars
|
||||
| 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`.
|
||||
All filenames lex-sort chronologically. Derive the ISO week (`YYYY-Www`) and month (`YYYY-MM`) from the conversation's `currentDate` and check whether that period's note already exists before offering a rollup.
|
||||
|
||||
A weekly/monthly rollup is a **light digest** — open threads across `projects/active/`, inbox items aging past ~7 days, and the period's `## Scope History` changes from `current-context.md`. It is *not* a vault-health audit (that's an agent-maintenance artifact — see below).
|
||||
|
||||
@@ -346,7 +278,7 @@ On the first substantive session of a calendar month, run a quick health pass an
|
||||
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"
|
||||
ECHO_TODAY=<currentDate> python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/vault_lint.py"
|
||||
```
|
||||
|
||||
Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Checks (the linter asserts each and prints violations):
|
||||
@@ -359,6 +291,18 @@ Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapp
|
||||
6. **Duplicate `## Agent Log` headings** — any daily note with more than one.
|
||||
7. **Unknown / retired paths** — any vault file that matches no route in `scripts/routing.json` (or sits at a retired path).
|
||||
8. **Frontmatter integrity** — missing required fields, `updated` < `created`, future `updated`, and `[[wikilinks]]` leaking into `source_notes`.
|
||||
9. **Graph health** — **broken wikilinks** (`[[X]]` resolving to no note), **orphans** (an entity note with no inbound links and an empty `## Related`), and **index drift** (entity-index entries whose file is gone, or indexable notes missing from `entities.json` — fix with `sweep.py`).
|
||||
|
||||
## Vault Maintenance — `sweep.py` (bring the vault up to spec)
|
||||
|
||||
After a plugin upgrade, run the sweep once to back-fill what older vaults lack — it (1) creates `_agent/index/`, (2) **rebuilds the entity index** from the notes already present, (3) **symmetrizes cross-links** (for every `## Related` A→B, adds the missing B→A; it never invents new links from body text), and (4) stamps the marker `schema_version` to current. Dry-run by default:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" # plan (read-only)
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" --apply # write
|
||||
```
|
||||
|
||||
Run it after `migrate.py` (which handles structural/schema changes) — or any time `/echo-health` reports index drift. It is idempotent; re-running is safe.
|
||||
|
||||
The pass is cheap and pays for itself by catching drift before it requires a reorg. Write the findings as a digest; act on them only with Jason's go-ahead.
|
||||
|
||||
@@ -366,46 +310,15 @@ The pass is cheap and pays for itself by catching drift before it requires a reo
|
||||
|
||||
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):**
|
||||
**Procedure (resilient)** — all dates are the conversation's `currentDate`, not the machine clock:
|
||||
|
||||
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).
|
||||
1. `python3 "$ECHO" get journal/daily/<currentDate>.md`.
|
||||
2. **404** — fetch `journal/templates/daily-note-template.md`, substitute its `{{date:YYYY-MM-DD}}` tokens to `<currentDate>`, write the result to a file (Write tool), and `put` it to the daily path; then go to step 4.
|
||||
3. **200 but no `## Agent Log`** — detect by matching an anchored `^## Agent Log` line in the **raw markdown** you just fetched. Do **not** check the document map, whose headings are full `::`-delimited paths (e.g. `2026-06-07::Agent Log`), so a bare `Agent Log` never matches and you'd append a duplicate heading. If missing, `post` a body of exactly `\n\n## Agent Log\n` to add the heading.
|
||||
4. PATCH-append the entry under `<currentDate>::Agent Log` (the daily note's H1 is the date, so that is the full target path):
|
||||
|
||||
```bash
|
||||
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"
|
||||
python3 "$ECHO" patch journal/daily/<currentDate>.md append heading "<currentDate>::Agent Log" <bodyfile>
|
||||
```
|
||||
|
||||
## Where to Write
|
||||
@@ -451,33 +364,21 @@ At the end of substantive conversations (ones that produced decisions, artifacts
|
||||
|
||||
**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.
|
||||
Write the body (see `references/session-log-template.md`) to a file with the Write tool, then:
|
||||
|
||||
```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"
|
||||
python3 "$ECHO" put _agent/sessions/<currentDate>-HHMM-<slug>.md <bodyfile>
|
||||
```
|
||||
|
||||
Then add a one-line entry to today's daily note via the **Daily Note — Agent Log** procedure above.
|
||||
|
||||
Finally, update the heartbeat pointer so the next session can orient in one GET (this is what closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read):
|
||||
Finally, update the heartbeat pointer so the next session can orient in one read (this closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read). It is a single line, `<session-log-path> @ <ISO-timestamp>`; write it to a file and PUT it:
|
||||
|
||||
```bash
|
||||
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"
|
||||
python3 "$ECHO" put _agent/heartbeat/last-session.md <bodyfile>
|
||||
```
|
||||
|
||||
`last-session.md` is a single-line pointer overwritten (PUT) each session end — never appended, so it can't grow or duplicate.
|
||||
`last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate.
|
||||
|
||||
## Vault Unreachable
|
||||
|
||||
|
||||
Reference in New Issue
Block a user