Skip to content

Commit 4fc47a2

Browse files
visahakclaude
andcommitted
feat(bob): implement manifest-first entity recall (#224)
Replace full-body entity injection with human-readable manifest output in Bob's recall script. Uses shared load_manifest and dedupe helpers from entity_io.py. Output format is markdown lines with path, type, and trigger — Bob reads full files on demand via read_file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ae26dce commit 4fc47a2

3 files changed

Lines changed: 45 additions & 94 deletions

File tree

platform-integrations/bob/evolve-lite/skills/evolve-lite:recall/SKILL.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ Entities can come from multiple sources:
1616

1717
## How It Works
1818

19-
1. List all `.md` files under `.evolve/entities/`, `.evolve/public/`, and their subdirectories
20-
2. Read each file — the YAML frontmatter contains `type` and `trigger`, the body contains the entity content and rationale
21-
3. Review each entity for relevance to the current task
22-
4. Apply relevant entities as additional context for your work
19+
1. The script scans `.evolve/entities/` and `.evolve/public/` and emits a compact manifest containing only `path`, `type`, and `trigger` for each entity
20+
2. Review the manifest and identify entities whose trigger looks relevant to the current task
21+
3. Use `read_file` to read the full content of relevant entity files on demand
22+
4. Apply the retrieved guidance as additional context for your work
2323

2424
**Directory structure**:
2525
- `.evolve/entities/guideline/` - Your private entities
@@ -54,7 +54,14 @@ Entities are stored as individual markdown files in `.evolve/entities/`, organiz
5454
code-review.md
5555
```
5656

57-
Each file uses markdown with YAML frontmatter:
57+
The manifest output is human-readable:
58+
59+
```
60+
- `.evolve/entities/guideline/use-context-managers.md` [guideline] — When processing files or managing resources
61+
- `.evolve/entities/subscribed/alice/guideline/error-handling.md` [guideline] — When writing error handlers
62+
```
63+
64+
Each file still uses markdown with YAML frontmatter:
5865

5966
```markdown
6067
---
@@ -71,11 +78,6 @@ Use context managers for file operations
7178
Ensures proper resource cleanup
7279
```
7380

74-
## Entity Annotations
81+
## On-Demand Expansion
7582

76-
Subscribed entities are annotated with their source:
77-
```
78-
- **[guideline]** [from: alice] Use context managers for file operations
79-
- _Rationale: Ensures proper resource cleanup_
80-
- _When: When processing files or managing resources_
81-
```
83+
When a manifest entry's trigger matches the current task, use `read_file` to load the full entity. The file body contains the guideline content and an optional `## Rationale` section.
Lines changed: 14 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
"""Retrieve and output entities for Bob to filter."""
2+
"""Retrieve and output an entity manifest for Bob to expand on demand."""
33

44
import sys
55
from pathlib import Path
@@ -12,99 +12,47 @@
1212
sys.path.insert(0, str(lib_path))
1313
break
1414

15-
from entity_io import find_entities_dir, get_evolve_dir, markdown_to_entity, log as _log # noqa: E402
15+
from entity_io import dedupe_manifest_entries, find_recall_entity_dirs, load_manifest, log as _log # noqa: E402
1616

1717

1818
def log(message):
1919
_log("retrieve", message)
2020

2121

2222
def format_entities(entities):
23-
"""Format all entities for Bob to review.
23+
"""Format a manifest of entities as human-readable markdown for Bob."""
24+
header = """## Evolve entity manifest for this task
2425
25-
Entities that came from a subscribed source have their path recorded in
26-
the private ``_source`` key (set by load_entities_with_source). These are
27-
annotated with ``[from: {name}]`` so Bob knows their provenance.
28-
"""
29-
header = """## Entities for this task
30-
31-
Review these entities and apply any relevant ones:
26+
These stored entities are available for this repo. Read only the files whose trigger looks relevant to the user's request:
3227
3328
"""
34-
items = []
29+
lines = []
3530
for e in entities:
36-
content = e.get("content")
37-
if not content:
38-
continue
39-
source = e.get("_source")
40-
if source:
41-
content = f"[from: {source}] {content}"
42-
item = f"- **[{e.get('type', 'general')}]** {content}"
43-
if e.get("rationale"):
44-
item += f"\n - _Rationale: {e['rationale']}_"
45-
if e.get("trigger"):
46-
item += f"\n - _When: {e['trigger']}_"
47-
items.append(item)
48-
49-
return header + "\n".join(items)
50-
51-
52-
def load_entities_with_source(entities_dir):
53-
"""Glob all .md files under entities_dir and parse each.
54-
55-
Entities stored under entities/subscribed/{name}/ have ``_source`` set to
56-
the subscription name so format_entities can annotate them. The owner field
57-
written by publish.py is preserved; _source is just a routing key used
58-
internally and is never written to disk.
59-
"""
60-
entities_dir = Path(entities_dir)
61-
entities = []
62-
for md in sorted(entities_dir.glob("**/*.md")):
63-
if md.is_symlink():
64-
continue
65-
try:
66-
entity = markdown_to_entity(md)
67-
entity.pop("_source", None)
68-
if not entity.get("content"):
69-
continue
70-
try:
71-
rel_parts = md.relative_to(entities_dir).parts
72-
except ValueError:
73-
rel_parts = md.parts
74-
if rel_parts[0] == "subscribed" and len(rel_parts) > 1:
75-
entity["_source"] = rel_parts[1]
76-
entities.append(entity)
77-
except (OSError, UnicodeDecodeError):
78-
pass
79-
return entities
31+
lines.append(f"- `{e['path']}` [{e['type']}] \u2014 {e['trigger']}")
32+
return header + "\n".join(lines)
8033

8134

8235
def main():
8336
log("Script started")
8437

85-
entities_dir = find_entities_dir()
86-
log(f"Entities dir: {entities_dir}")
87-
8838
entities = []
89-
if entities_dir:
90-
entities = load_entities_with_source(entities_dir)
39+
recall_dirs = find_recall_entity_dirs()
40+
log(f"Recall dirs: {recall_dirs}")
41+
for root_dir in recall_dirs:
42+
entities.extend(load_manifest(root_dir))
9143

92-
public_dir = get_evolve_dir() / "public"
93-
if public_dir.is_dir():
94-
log(f"Loading public entities from: {public_dir}")
95-
entities += load_entities_with_source(public_dir)
44+
entities = dedupe_manifest_entries(entities)
9645

9746
if not entities:
9847
log("No entities found")
9948
return
10049

10150
log(f"Loaded {len(entities)} entities")
51+
10252
output = format_entities(entities)
10353
print(output)
10454
log(f"Output {len(output)} chars to stdout")
10555

10656

10757
if __name__ == "__main__":
10858
main()
109-
110-
# Made with Bob

tests/platform_integrations/test_bob_sharing.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -610,51 +610,52 @@ def test_output_reports_added_count(self, temp_project_dir):
610610
class TestBobRetrieveEntities:
611611
"""Tests for Bob's retrieve_entities.py script.
612612
613-
Note: Bob's retrieve script outputs markdown for Bob's UI, not JSON.
613+
Bob outputs human-readable manifest markdown (not JSON like Claude/Codex).
614614
"""
615615

616616
def test_returns_entities_from_private_dir(self, temp_project_dir):
617617
evolve_dir = temp_project_dir / ".evolve"
618618
entities_dir = evolve_dir / "entities" / "guideline"
619619
entities_dir.mkdir(parents=True)
620-
(entities_dir / "tip.md").write_text("---\ntype: guideline\n---\n\nPrivate tip.\n")
620+
(entities_dir / "tip.md").write_text("---\ntype: guideline\ntrigger: when writing private code\n---\n\nPrivate tip.\n")
621621

622622
result = run_script(RETRIEVE_SCRIPT, temp_project_dir, evolve_dir=evolve_dir)
623-
# Bob outputs markdown, not JSON
624-
assert "Private tip" in result.stdout
625-
assert "## Entities for this task" in result.stdout
623+
assert "Evolve entity manifest for this task" in result.stdout
624+
assert "[guideline]" in result.stdout
625+
assert "when writing private code" in result.stdout
626+
assert "Private tip." not in result.stdout
626627

627628
def test_returns_entities_from_public_dir(self, temp_project_dir):
628629
evolve_dir = temp_project_dir / ".evolve"
629630
public_dir = evolve_dir / "public" / "guideline"
630631
public_dir.mkdir(parents=True)
631-
(public_dir / "tip.md").write_text("---\ntype: guideline\nvisibility: public\n---\n\nPublic tip.\n")
632+
(public_dir / "tip.md").write_text("---\ntype: guideline\ntrigger: when sharing guidelines\nvisibility: public\n---\n\nPublic tip.\n")
632633

633634
result = run_script(RETRIEVE_SCRIPT, temp_project_dir, evolve_dir=evolve_dir)
634-
assert "Public tip" in result.stdout
635+
assert "when sharing guidelines" in result.stdout
636+
assert "Public tip." not in result.stdout
635637

636638
def test_returns_entities_from_subscribed_dir(self, temp_project_dir):
637639
evolve_dir = temp_project_dir / ".evolve"
638640
subscribed_dir = evolve_dir / "entities" / "subscribed" / "alice" / "guideline"
639641
subscribed_dir.mkdir(parents=True)
640-
(subscribed_dir / "tip.md").write_text("---\ntype: guideline\n---\n\nSubscribed tip.\n")
642+
(subscribed_dir / "tip.md").write_text("---\ntype: guideline\ntrigger: when adding coverage\n---\n\nSubscribed tip.\n")
641643

642644
result = run_script(RETRIEVE_SCRIPT, temp_project_dir, evolve_dir=evolve_dir)
643-
assert "Subscribed tip" in result.stdout
644-
assert "[from: alice]" in result.stdout
645+
assert "when adding coverage" in result.stdout
646+
assert ".evolve/entities/subscribed/alice/guideline/tip.md" in result.stdout
647+
assert "Subscribed tip." not in result.stdout
645648

646649
def test_retrieve_filters_symlinked_entities(self, temp_project_dir):
647650
evolve_dir = temp_project_dir / ".evolve"
648651
subscribed_dir = evolve_dir / "entities" / "subscribed" / "alice" / "guideline"
649652
subscribed_dir.mkdir(parents=True)
650653
real_file = subscribed_dir / "real.md"
651-
real_file.write_text("---\ntype: guideline\n---\n\nReal content.\n")
654+
real_file.write_text("---\ntype: guideline\ntrigger: when testing\n---\n\nReal content.\n")
652655
link_file = subscribed_dir / "link.md"
653656
link_file.symlink_to(real_file)
654657

655658
result = run_script(RETRIEVE_SCRIPT, temp_project_dir, evolve_dir=evolve_dir)
656-
assert "Real content" in result.stdout
657-
assert result.stdout.count("Real content") == 1, "Symlinked duplicate should be filtered out"
658-
659-
660-
# Made with Bob
659+
assert "when testing" in result.stdout
660+
assert result.stdout.count("when testing") == 1, "Symlinked duplicate should be filtered out"
661+
assert "Real content." not in result.stdout

0 commit comments

Comments
 (0)