Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
63ddef1
refactor(platform-integrations): always-on EVOLVE.md for Bob+Codex (n…
illeatmyhat Jun 8, 2026
39a1252
feat(platform-integrations): native-memory adapter + thin Claude EVOL…
illeatmyhat Jun 8, 2026
52593b4
feat(platform-integrations): detect silently-disabled Claude EVOLVE.m…
illeatmyhat Jun 8, 2026
2a81a66
refactor(platform-integrations): drop all Claude auto-firing hooks (f…
illeatmyhat Jun 8, 2026
ac3aa54
feat(platform-integrations): stable native-to-entity id linkage for p…
illeatmyhat Jun 8, 2026
1526e9d
feat(platform-integrations): automate provenance matching with native…
illeatmyhat Jun 8, 2026
beb83a4
test(platform-integrations): end-to-end chain test + run provenance/e…
illeatmyhat Jun 8, 2026
204dd4a
refactor(platform-integrations): ship audit_recall.py from lib/ not a…
illeatmyhat Jun 9, 2026
b478f2a
fix(platform-integrations): address CodeRabbit review on PR #266
illeatmyhat Jun 9, 2026
5544863
fix(platform-integrations): scope Claude-only doctor skill out of cod…
illeatmyhat Jun 9, 2026
689bf3b
feat(platform-integrations): make uninstall reverse legacy pre-redesi…
illeatmyhat Jun 9, 2026
f3faa9a
feat(platform-integrations): ship adapt_memory to stable path + auto-…
illeatmyhat Jun 9, 2026
a0db603
fix(platform-integrations): scope the adapt-memory skill to mirror-on…
illeatmyhat Jun 10, 2026
2dc7e06
feat(platform-integrations): adapt-memory auto-locates the saved nati…
illeatmyhat Jun 10, 2026
6599f16
feat(platform-integrations): build recall+learn out of the Claude plu…
illeatmyhat Jun 10, 2026
0904f55
fix(platform-integrations): forbid native-memory-store inspection in …
illeatmyhat Jun 10, 2026
9b4d845
fix(platform-integrations): unify codex/bob self-directed memory onto…
illeatmyhat Jun 10, 2026
6cce8f8
feat(platform-integrations): locate native Codex session transcripts …
illeatmyhat Jun 10, 2026
eb73854
feat(platform-integrations): auto-allowlist the recall-audit command …
illeatmyhat Jun 10, 2026
01bb0a3
fix(platform-integrations): make codex/bob recall an imperative first…
illeatmyhat Jun 10, 2026
5885157
feat(platform-integrations): close Bob provenance via a session-id br…
illeatmyhat Jun 10, 2026
e0e0681
test/hardening: robustify Bob cwd-hash + cover codex/pwd-fallback loc…
illeatmyhat Jun 10, 2026
8c0066f
fix(platform-integrations): guard non-dict chat JSON in Bob session-i…
illeatmyhat Jun 10, 2026
959f94b
fix(platform-integrations): harden provenance locators against decoy/…
illeatmyhat Jun 11, 2026
c7a6a56
chore(platform-integrations): drop vestigial bob-lite custom_modes.yaml
illeatmyhat Jun 12, 2026
e8bd3ff
refactor(platform-integrations): exclude recall/learn skills from cod…
illeatmyhat Jun 15, 2026
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
87 changes: 87 additions & 0 deletions platform-integrations/bob/evolve-lite/EVOLVE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Evolve — self-directed memory

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

## Recall — your first action, before any other tool use

On a non-trivial task (code changes, debugging, repo exploration, or
environment/tooling investigation), your FIRST action — before reading source,
running commands, or anything else — is to check `./.evolve/entities/`: list
that directory, read each entity's `trigger` line, and open the entity files
whose trigger matches the task. The moment you open one or more entities, your
next step — still part of this same first action — is to record them with the
audit command under "Record what you consulted" below; do it before you move on
to the task. Let what you find inform the work that follows. If the directory is
missing or nothing matches, note that and proceed — that's normal.

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

## Record what you consulted

Whenever you opened entities in the recall step above, record them now — run
this before doing anything else, so the value of this memory can be measured
over time:

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

Pass the entity id `<type>/<name>` for each entity you consulted, where `<type>`
is its directory under `entities/` and `<name>` is its filename without `.md`
(e.g. `project/test-fixture-generated`). Skip this step entirely if you
consulted no entities. 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 as an entity.
Saving nothing is the right outcome more often than not; never force a
low-value entity just to have saved one.

Each entity is one file holding one fact, at
`./.evolve/entities/<type>/<short-kebab-slug>.md` (create the directory if it
doesn't exist — `<type>` is one of the types below). The filename is the
entity's name; the frontmatter carries its type and trigger:

```markdown
---
type: <user | feedback | project | reference>
trigger: <one line naming the situation in which a future session should recall this>
---

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

The `trigger` is what a future session matches against during recall, so make it
about *when* the fact applies, not just what it is.

Types (the `<type>` directory and frontmatter value):
- **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 entities with `[[name]]`, where `name` is another
entity's filename slug. Link liberally; a `[[name]]` with no file yet marks
something worth writing later, not an error.

## 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 entity that already covers it — update
that file rather than creating a duplicate.
- Delete entities 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.

This file was deleted.

This file was deleted.

71 changes: 0 additions & 71 deletions platform-integrations/bob/evolve-lite/custom_modes.yaml

This file was deleted.

109 changes: 109 additions & 0 deletions platform-integrations/bob/evolve-lite/lib/evolve-lite/audit_recall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/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 hashlib
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 _bob_session_id() -> str | None:
"""Recover Bob's real session id for the current run.

Bob (a Gemini-CLI fork) exposes no session-id environment variable to tool
subprocesses, but it writes the live session to
``~/.bob/tmp/<sha256(cwd)>/chats/session-<ts>-<sid8>.json`` with a real
``sessionId`` field (the filename's trailing segment is that id's first
block). Recovering it lets `provenance` tie this recall to the saved
trajectory instead of an opaque minted uuid. Gated on ``BOBSHELL_CLI`` so it
is inert on every other host. Returns the id, or ``None`` when not under Bob
or no chat file is found (caller then mints a uuid)."""
if not os.environ.get("BOBSHELL_CLI"):
return None
try:
# Bob hashes the project path it was launched in. os.getcwd() returns
# the resolved (symlink-free) path, but Bob may have captured the
# symlinked path the user cd'd through; $PWD preserves that. Try both
# candidate hashes and pick the newest chat across them.
chats = []
seen_paths: set[str] = set()
for raw in (os.getcwd(), os.environ.get("PWD")):
if not raw or raw in seen_paths:
continue
seen_paths.add(raw)
project_hash = hashlib.sha256(raw.encode()).hexdigest()
chats.extend((Path.home() / ".bob" / "tmp" / project_hash / "chats").glob("session-*.json"))
for chat in sorted(chats, key=lambda p: p.stat().st_mtime, reverse=True):
try:
data = json.loads(chat.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
sid = data.get("sessionId") if isinstance(data, dict) else None
if sid:
return str(sid)
except OSError:
return None
return None


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
bob_sid = _bob_session_id()
if bob_sid:
return bob_sid, 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:]))
73 changes: 66 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,47 @@ def slugify(text, max_length=60):
return text or "entity"


def claude_project_slug(path):
"""Derive Claude's per-project directory name from an absolute path.

Claude names a project's ``~/.claude/projects/<slug>/`` directory by
replacing every non-alphanumeric character in the resolved absolute project
path with ``-``.

>>> claude_project_slug("/Users/x/evolve-smoke-test2")
'-Users-x-evolve-smoke-test2'

This is the single source of truth shared by doctor.py (transcript dir) and
adapt_memory.py (native memory dir).
"""
return re.sub(r"[^A-Za-z0-9]", "-", str(Path(path).resolve()))


def claude_memory_dir(path, home=None):
"""Return the native Claude memory dir for the project rooted at *path*.

``~/.claude/projects/<slug>/memory/`` where ``<slug>`` is
:func:`claude_project_slug` of *path*. *home* defaults to ``Path.home()``.
"""
home = Path.home() if home is None else Path(home)
return home / ".claude" / "projects" / claude_project_slug(path) / "memory"


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 +180,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 +380,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 +419,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
Loading
Loading