Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions platform-integrations/bob/evolve-lite/EVOLVE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Evolve — self-directed memory

You have a persistent, file-based memory for the current project, stored under
`./.evolve/memory/` (relative to the workspace/project root). You decide, on
your own judgment, when something is worth remembering — nothing forces a save,
and there is no step to "complete." Curate this memory like notes you'll thank
yourself for later: small, accurate, high-signal.

## Recall — at the start of a non-trivial task

Before substantive work (code changes, debugging, repo exploration, or
environment/tooling investigation), read your memory index at
`./.evolve/memory/MEMORY.md` if it exists. It holds one line per memory with a
short description. Open the individual memory files whose description looks
relevant to the task at hand, and let them inform what you do. If the index is
missing or nothing looks relevant, just proceed — that's normal.

Memories reflect what was true when written. If a memory names a file,
function, command, or flag, verify it still exists before relying on it.

## Record what you consulted

After recall, log which entries you actually opened, so the value of this memory
can be measured over time. Run:

```bash
python3 ~/.bob/evolve-lite/audit_recall.py <file> [<file> ...]
```

Pass the memory files you read this turn (space-separated paths, relative to the
project root). Skip this step entirely if you consulted no memories. If the
command prints a line beginning `evolve-session:`, include that line once,
verbatim, somewhere in your reply — it lets later analysis tie this session to
what you recalled.

## Save — only when you learn something durable

Near the end of a task, if it produced a reusable fact that isn't already
obvious from the code or git history — and only then — write it to memory.
Saving nothing is the right outcome more often than not; never force a
low-value memory just to have saved one.

Each memory is one file holding one fact, under `./.evolve/memory/` (create the
directory if it doesn't exist), with frontmatter:

```markdown
---
name: <short-kebab-case-slug>
description: <one-line summary — used to decide relevance during recall>
metadata:
type: user | feedback | project | reference
---

<the fact. For feedback/project, follow with **Why:** and **How to apply:** lines.
Link related memories with [[their-name]].>
```

Types:
- **user** — who the user is: role, expertise, durable preferences.
- **feedback** — guidance on how you should work, both corrections and
confirmed approaches; always include the why.
- **project** — ongoing work, goals, or constraints not derivable from the code
or git history; convert relative dates ("next week") to absolute ones.
- **reference** — pointers to external resources (URLs, dashboards, tickets).

In the body, link related memories with `[[name]]`, where `name` is another
memory's `name:` slug. Link liberally; a `[[name]]` with no file yet marks
something worth writing later, not an error.

After writing the file, add a one-line pointer to `./.evolve/memory/MEMORY.md`:
`- [Title](file.md) — short hook`. MEMORY.md is the index you read during
recall — one line per memory, no frontmatter, never put memory content there.

## When NOT to save, and housekeeping

- Don't duplicate what the repo already records: code structure, git history,
READMEs, existing docs. If asked to remember one of those, ask what was
non-obvious about it and save that instead.
- Don't save what only matters to the current conversation.
- Before saving, check for an existing memory that already covers it — update
that file rather than creating a duplicate.
- Delete memories that turn out to be wrong.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
description: Mirror a just-saved native memory into the shared evolve store so it becomes shareable and auditable
---
Use the `evolve-lite-adapt-memory` skill on the current conversation. Follow the skill's instructions exactly.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Append a recall-audit row to .evolve/audit.log.

Self-contained (no third-party or evolve-lite lib imports) so it can be dropped
at a single path and run by a model-invoked shell command on any platform.

Usage:
python3 audit_recall.py <memory_file> [<memory_file> ...]

Records which memory entries the model consulted this turn so the `provenance`
analysis can later judge whether they influenced the outcome. Session id is
resolved from the host's environment when available and falls back to a freshly
minted UUID (printed as `evolve-session: <id>` for the model to echo).
"""

from __future__ import annotations

import json
import os
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path


def _evolve_dir() -> Path:
env = os.environ.get("EVOLVE_DIR")
return Path(env) if env else Path.cwd() / ".evolve"


def _session_id() -> tuple[str, bool]:
"""Return (session_id, self_minted)."""
for var in ("CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"):
val = os.environ.get(var)
if val:
return val, False
return str(uuid.uuid4()), True


def main(argv: list[str]) -> int:
entities = [a for a in argv if a.strip()]
if not entities:
return 0

session_id, minted = _session_id()
row = {
"event": "recall",
"session_id": session_id,
"entities": entities,
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
}

log = _evolve_dir() / "audit.log"
log.parent.mkdir(parents=True, exist_ok=True)
with log.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(row) + "\n")

if minted:
print(f"evolve-session: {session_id}")
count = len(entities)
print(f"Recorded recall of {count} memory entr{'y' if count == 1 else 'ies'}.")
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
47 changes: 40 additions & 7 deletions platform-integrations/bob/evolve-lite/lib/evolve-lite/entity_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,21 @@ def slugify(text, max_length=60):
return text or "entity"


def sanitize_type(text):
"""Sanitize an entity *type* into a filesystem-safe subdirectory name.

Like :func:`slugify` but without truncation — a type is a short label,
not free-form content, and truncating it could silently merge distinct
types. Returns an empty string for input that contains no usable
characters, leaving the fallback decision to the caller.
"""
if not isinstance(text, str):
return ""
text = text.lower()
text = re.sub(r"[^a-z0-9]+", "-", text)
return text.strip("-")


def unique_filename(directory, slug):
"""Return a Path that doesn't collide with existing files in *directory*.

Expand All @@ -139,7 +154,7 @@ def unique_filename(directory, slug):
# Markdown <-> dict conversion
# ---------------------------------------------------------------------------

_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "visibility", "published_at")
_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "native_path", "visibility", "published_at")


def entity_to_markdown(entity):
Expand Down Expand Up @@ -339,24 +354,35 @@ def load_all_entities(entities_dir):
return entities


def write_entity_file(directory, entity):
def write_entity_file(directory, entity, filename=None, overwrite=False):
"""Write a single entity as a markdown file under *directory*.

The file is placed in a ``{type}/`` subdirectory. Uses atomic
write (write to ``.tmp``, then ``os.rename``).

Args:
directory: Entities root directory.
entity: The entity dict to serialize.
filename: Optional explicit slug for the target file (without the
``.md`` suffix). When omitted, the slug is derived from the
entity content (the historical default).
overwrite: When True, the entity is written to a deterministic
``{type}/{filename}.md`` path, overwriting any existing file in
place (stable id, idempotent re-mirroring). When False (the
default), the historical collision-avoiding behavior is kept —
a ``-2``/``-3`` suffix is appended on collision.

Returns:
Path to the written file.
"""
_ALLOWED_TYPES = {"guideline", "preference"}
entity_type = entity.get("type", "guideline")
if not isinstance(entity_type, str) or entity_type not in _ALLOWED_TYPES:
entity_type = "guideline"
# Any non-empty type is accepted and used (sanitized) as the
# subdirectory. An empty/invalid type falls back to "guideline".
entity_type = sanitize_type(entity.get("type", "guideline")) or "guideline"
entity["type"] = entity_type
type_dir = Path(directory) / entity_type
type_dir.mkdir(parents=True, exist_ok=True)

slug = slugify(entity.get("content", "entity"))
slug = slugify(filename) if filename else slugify(entity.get("content", "entity"))
content = entity_to_markdown(entity)

# Write to a unique temp file first (avoids predictable .tmp collisions)
Expand All @@ -367,6 +393,13 @@ def write_entity_file(directory, entity):
os.close(fd)
fd = None

if overwrite:
# Deterministic target: overwrite any existing file in place so
# the entity id is stable across re-mirroring.
target = type_dir / f"{slug}.md"
os.replace(tmp_path, target)
return target

# Atomically claim the target using O_EXCL; retry on race
while True:
target = unique_filename(type_dir, slug)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: evolve-lite:adapt-memory
description: Mirror a just-saved native memory into the shared evolve store so it becomes shareable and auditable
---

# Adapt Memory

This skill mirrors a just-saved native memory into the shared evolve store. It
is specific to hosts with native self-directed memory and is a no-op on this
platform — there is no native memory store to mirror from. Use the
`evolve-lite:learn`
skill to capture reusable lessons here.

Loading
Loading