Files
echo/CHANGELOG.md
T
Jason Stedwell be3fd36ebf ver 1.5.0 — six-improvement pass: retention, routing, self-firing memory
1. capture-update keeps the whole body (was truncating to first line)
2. frontmatter completeness: kind-default status + kind-seeded tags at
   capture, fm create-or-replace, incomplete-frontmatter lint, sweep
   backfill (one data source: KIND_STATUS/KIND_REQUIRED_FM)
3. pre-write duplicate gate (exit 76, --merge-into/--force)
4. recall corpus + ranking: sessions/journal indexed (down-weighted),
   BM25 x freshness x status fusion, recall --json, index schema 2
5. session hooks: SessionStart auto-load, Stop reflection nudge
6. one-tap triage verb with processing-log audit; --json on read verbs

+22 mock end-to-end tests; all suites green. Docs current (README,
CHANGELOG, SKILL.md, commands, references, MAINTENANCE, eval README).
Drops tracked .DS_Store (gitignored); retires README-gretchen.md
(moved to gitignored dist/); adds the field report that drove item 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:57:52 -05:00

145 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## 1.5.0
Six improvements from the July 2026 review + the frontmatter-drift field report.
### Fixed — capture-on-update kept only the first body line (silent data loss)
`capture` on an existing entity appended `- <date>: <first line>` and **discarded the
rest of the body**. It now appends the whole body as a dated block — first line on the
bullet, continuation lines indented under it. Idempotency (bullet-per-day) unchanged.
### Added — frontmatter completeness (prevent → tool → detect → remediate)
A field audit found 18/71 entity notes missing `status`/`tags`; the drift was created
at write time and invisible to the tooling. Four coordinated changes, driven by one
data source (`KIND_STATUS` / `KIND_REQUIRED_FM` in `echo_index.py`):
- **`capture`** stamps a kind-appropriate default `status` (all kinds, not just
projects) and seeds `tags` with the note's kind; new `--tags a,b` enriches it.
Reflect/triage proposals accept a `"tags"` field.
- **`echo.py fm`** is now **create-or-replace**: on `400 invalid-target` (missing key)
it surgically inserts the one line into the frontmatter block and re-PUTs, verified —
it previously failed on exactly the notes that needed repair.
- **`vault_lint.py`** gains a kind-scoped `incomplete-frontmatter` check
(missing/empty `status`/`tags` on entity notes; block-style lists handled).
- **`sweep.py --apply`** backfills flagged notes (kind-default status + baseline tag).
### Added — pre-write duplicate gate in capture
A strong fuzzy candidate (score ≥ 0.6, env `ECHO_DUP_GATE`) now **stops** `capture`
before it creates a near-duplicate (exit `76` + candidate list) instead of warning
after the file exists. `--merge-into <slug>` routes the capture as an update to the
existing entity (mention learned as an alias); `--force` creates anyway. Weak
resemblances still create + warn as before.
### Changed — recall corpus + ranking (recall-index schema 2)
- The BM25 corpus now includes **session logs and journal notes** (down-weighted 0.5 /
0.4 so entity notes win ties) — past discussions are findable without having been
promoted into an entity note. `echo.py put` keeps the index current for corpus paths.
- Ranking fuses BM25 with **freshness** (half-life decay on `updated:`, floored at 0.6,
env `ECHO_FRESH_HALF_LIFE`) and **status** (`active` ×1.1, `on-hold` ×0.9,
`archived` ×0.75) — a stale archived note no longer outranks the live one.
- Hits print `updated:`/`status:`; `recall --json` emits structured results.
- The recall index carries per-doc meta (schema 2); an old index is discarded and
rebuilt automatically on the next recall/sweep.
### Added — session hooks (self-firing load & reflect)
New `hooks/hooks.json` + two hook scripts. **SessionStart** runs `echo.py load` and
injects the output as context (skips resume/compact; NOT-CONFIGURED degrades to an
instruction, never an error). **Stop** nudges once per substantive session (≥5 user
turns, env `ECHO_REFLECT_MIN_TURNS`) when nothing was reflected or session-logged —
reflection itself still previews and asks before writing. Both use the 1.4.2 CoWork
path fallback and always exit 0.
### Added — one-tap inbox triage + `--json` on read verbs
New `echo.py triage`: `--list --json` parses the inbox into `{line, date, text,
age_days}`; a proposals file (reflect schema + `"line"`) is classified → previewed →
applied via `capture`, with the `inbox/processing-log/` audit line written per move
(originals never deleted). `/echo-triage` rewritten around it. Also: `--json` for
`recall`, `link`, and `scope show` (`resolve`/`capture` already had it).
Tests: +22 feature cases (mock end-to-end incl. hooks), patch-semantics updated for
the new `fm` contract; all suites green. Docs: SKILL.md, README, command docs updated.
## 1.4.2
### Fixed — CoWork sandbox plugin-path resolution
In a remote CoWork sandbox, `${CLAUDE_PLUGIN_ROOT}` can resolve to a host path the
sandbox can't reach (e.g. `/var/folders/…`), while the plugin is actually mounted under
`…/mnt/.remote-plugins/…`. Every `python3 "${CLAUDE_PLUGIN_ROOT}/…"` invocation then
failed until the agent manually located the real path. (Root cause is the harness env
var, not the plugin — the scripts always used the variable correctly, never a hardcoded
path — but the plugin now self-heals.)
- SKILL.md and all eight slash commands resolve the scripts path with a fallback:
prefer `${CLAUDE_PLUGIN_ROOT}`, else locate the copy under
`/sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/`. Reused via
`$ECHO`/`$LINT`/`$SWEEP`/`$SDIR`; on a normal host the primary path always wins, so
behaviour is unchanged.
- `echo-health` / `echo-sweep` `allowed-tools` broadened to match the resolved
(variable-dir) invocation and the `ls`/`dirname` resolver steps.
## 1.4.1
### Fixed — baked credentials now win unconditionally
A per-user baked artifact could still be prompted for credentials on install. The
baked `DEFAULT_*` tier was lowest priority, so a stale `ECHO_*` env var or a
leftover/placeholder `~/.claude/echo-memory/config.json` shadowed the baked key —
even an unedited `"<placeholder>"` value won over it, flipping `is_configured()` to
false and triggering the first-run "ask the operator" flow despite valid baked creds.
- `echo_config.load()` now treats a **complete** baked set (endpoint + key both
present) as authoritative: it wins over env vars and the config file and cannot be
shadowed. The generic (unbaked) plugin keeps its env → file → first-run flow.
- Added `baked_complete()`; `source()` reports `baked-in (plugin)` for that tier.
### Security — untracked leaked baked artifacts
- Two labeled baked artifacts (`echo-memory-1.4.0-jason.plugin`,
`echo-memory-1.4.0-gretchen.plugin`), each carrying a live vault bearer token, had
been committed at the repo root. They are now `git rm --cached`-removed and
`.gitignore` gains `echo-memory-*-*.plugin` so labeled builds can't be re-committed
(the version-only pointer `echo-memory.plugin` and `echo-memory-<ver>.plugin` stay
tracked). Tokens remain in prior history — rotate if the repo was ever shared.
## 1.4.0
### Added — per-user baked-key builds (CoWork zero-touch)
The plugin is the only thing reliably mounted into a CoWork session (the sandbox's
`~/.claude` is synthetic, not your real one), so the config now *can* travel inside
the artifact. `echo_config` gains a lowest-priority fallback tier — the
`DEFAULT_OWNER` / `DEFAULT_BASE` / `DEFAULT_KEY` constants — **empty in source**, so
the committed tree still ships zero credentials.
- `build.py --bake-key --from <config.json> [--label <name>]` substitutes a single
user's owner/endpoint/key into those constants, producing a per-user artifact that
needs **no config file and no per-session paste**. Values may also come from
`--owner/--endpoint/--key` or `ECHO_OWNER/ECHO_BASE/ECHO_KEY`.
- Baked builds default to `dist/` (gitignored) and never touch the shared
`echo-memory.plugin` pointer.
- `build.py --strip-key` now force-blanks the same constants (guaranteed token-free).
- `config show` / `doctor` report `[baked-in default]` when a field resolves from the
baked tier.
### Changed — artifact hygiene
- `.gitignore` excludes `dist/` and the filled-in `echo-memory.config.json`, where
baked builds and the key file are meant to live. A baked artifact carries a vault
key and must be **delivered directly to that one user, never committed, pushed, or
published.** (Note: labeled baked artifacts built at the repo *root* were not yet
guarded — see 1.4.1.)
### Resolution order (unchanged for hosts)
`env``$ECHO_CONFIG` file → canonical `~/.claude/echo-memory/config.json`
**baked `DEFAULT_*`**. On a normal desktop the empty defaults mean behaviour is
identical to 1.3.x; the baked tier only matters in per-user artifacts.