subagent-driven-development
OBobra/superpowers298k近 7 天新增 2,631 个 star
Analyze a complete literary work into a structured Basic Memory knowledge graph: schema design, entity seeding, chapter-by-chapter processing, cross-referencing, and validation.
Transform a complete literary work into a structured knowledge graph. Characters, themes, chapters, locations, symbols, and literary devices become interconnected notes — searchable, validatable, and traversable.
Phase 0: Setup → project, schemas, directory structure
Phase 1: Seed → stub notes for known major entities
Phase 2: Process → chapter-by-chapter notes in batches
Phase 3: Cross-ref → enrich arcs, add parallels, write analysis
Phase 4: Validate → schema checks, drift detection, consistency
Phase 5: Explore → traverse the graph, write synthesis notes
Writing always goes through write_note and edit_note. For reading — which is most of
the work in a long analysis — prefer the POSIX read verbs where they are available
(enable_posix_tools for the MCP tools; the bm CLI verbs are always available):
| Need | Use | Instead of |
|---|---|---|
| A section of a long note | cat <note> --section Observations | reading the whole note |
| A line range of the source text | cat <source>.txt --lines 4200-4890 | pulling the whole book into context |
| Notes matching frontmatter | find --meta status=active | reading notes to check fields |
| Fields across many notes | find --meta ... --fields pov,setting | one read per note |
| Where something lives | ls, tree, find --name '*.md' | listing everything |
The two rules that matter across a 100+ chapter run:
--meta predicates and --fields
projection are for — one call answers what a read-per-note loop would cost.Two sharp edges to know before you write a query. The first fails quietly — a short answer, exit 0, no warning — so learn it here rather than from a graph you thought you had audited:
find pages, and the default page is 10. Any query whose answer is "all N chapters"
needs --page-size 200 (the maximum) — see Coverage Checks.--name cannot combine with --meta. The metadata search has no filename glob. Scope a
--meta query with the positional path instead: find /characters --meta 'note_type=character'. That path is matched on a directory boundary against the file path
a note is indexed under — where the note actually lives, not its permalink, which stops
mirroring the file path once a note pins permalink: in frontmatter or is moved. So
/characters reaches everything filed under characters/ (including characters/major/),
and never characters-cut/.If the POSIX verbs are unavailable, every step below still works with search_notes,
read_note, and list_directory — it just costs more.
create_memory_project(project_name="<work-name>", project_path="~/basic-memory/<work-name>")
Use a kebab-case slug of the work's title (e.g., great-gatsby, hamlet, beloved).
Write 6 schema notes to schema/. Each schema defines the entity type's fields, observation categories, and relation types. Adapt fields to fit the work — the schemas below are starting points, not rigid templates.
write_note(
title="Character",
directory="schema",
note_type="schema",
metadata={
"entity": "Character",
"version": 1,
"schema": {
"role(enum)": "[protagonist, antagonist, supporting, minor], character's narrative role",
"description": "string, brief character description",
"first_appearance?": "string, chapter or scene of first appearance",
"status?(enum)": "[alive, dead, unknown, transformed], character status at end of work"
},
"settings": {"validation": "warn"}
},
content="""# Character
Schema for character entity notes.
## Observations
- [convention] Major characters in characters/major/, minor in characters/minor/
- [convention] Observation categories: trait, motivation, arc, quote, appearance, relationship, symbolism, fate
- [convention] Relations: appears_in, contrasts_with, allied_with, commands, symbolizes, associated_with"""
)
Add work-specific fields as needed — e.g., rank for military fiction, house for family sagas, species for fantasy.
write_note(
title="Theme",
directory="schema",
note_type="schema",
metadata={
"entity": "Theme",
"version": 1,
"schema": {
"description": "string, what this theme explores",
"prevalence(enum)": "[major, minor], how central to the work",
"first_introduced?": "string, where theme first appears"
},
"settings": {"validation": "warn"}
},
content="""# Theme
Schema for thematic analysis notes.
## Observations
- [convention] Observation categories: definition, manifestation, evolution, counterpoint, quote, interpretation
- [convention] Relations: embodied_by, contrasts_with, reinforced_by, explored_in, expressed_through"""
)
write_note(
title="Chapter",
directory="schema",
note_type="schema",
metadata={
"entity": "Chapter",
"version": 1,
"schema": {
"chapter_number": "integer, sequential chapter number",
"pov?": "string, point-of-view character or narrator mode",
"setting?": "string, primary location",
"narrative_mode?(enum)": "[dramatic, expository, reflective, epistolary, mixed], chapter's primary mode"
},
"settings": {"validation": "warn"}
},
content="""# Chapter
Schema for chapter-level analysis notes.
## Observations
- [convention] Chapters stored in chapters/ directory
- [convention] Observation categories: summary, event, tone, technique, quote, significance, foreshadowing
- [convention] Relations: features, set_in, explores, contains, employs, follows, precedes, parallels"""
)
write_note(
title="Location",
directory="schema",
note_type="schema",
metadata={
"entity": "Location",
"version": 1,
"schema": {
"description": "string, what this place is",
"location_type(enum)": "[city, building, landscape, body_of_water, region, fictional, vehicle], type of place",
"real_or_fictional(enum)": "[real, fictional, both], whether the place exists"
},
"settings": {"validation": "warn"}
},
content="""# Location
Schema for location and setting notes.
## Observations
- [convention] Observation categories: description, atmosphere, symbolism, significance, geography
- [convention] Relations: setting_for, associated_with, symbolizes, contains, part_of"""
)
write_note(
title="Symbol",
directory="schema",
note_type="schema",
metadata={
"entity": "Symbol",
"version": 1,
"schema": {
"description": "string, what the symbol is literally",
"symbol_type(enum)": "[object, animal, color, action, natural_phenomenon, body_part], category of symbol",
"primary_meaning": "string, most common interpretation"
},
"settings": {"validation": "warn"}
},
content="""# Symbol
Schema for symbolic element notes.
## Observations
- [convention] Observation categories: meaning, appearance, ambiguity, interpretation, quote, evolution
- [convention] Relations: represents, associated_with, appears_in, contrasts_with, located_at"""
)
write_note(
title="LiteraryDevice",
directory="schema",
note_type="schema",
metadata={
"entity": "LiteraryDevice",
"version": 1,
"schema": {
"description": "string, what the device is",
"device_type(enum)": "[rhetorical, structural, figurative, narrative, dramatic], category",
"frequency(enum)": "[pervasive, frequent, occasional, rare], how often used"
},
"settings": {"validation": "warn"}
},
content="""# LiteraryDevice
Schema for literary technique and device notes.
## Observations
- [convention] Observation categories: definition, usage, effect, example, significance
- [convention] Relations: used_in, characterizes, expresses, related_to"""
)
<project>/
<work>.txt # the source text, verbatim (see Phase 2)
schema/ # 6 schema definitions
chapters/ # one note per chapter/section + prologue/epilogue
characters/
major/ # protagonist, antagonist, key supporting
minor/ # named characters with limited roles
themes/ # thematic analysis notes
locations/ # settings and places
symbols/ # symbolic elements
literary-devices/ # techniques and devices
analysis/ # cross-cutting synthesis
tasks/ # processing tracker
Before processing chapters, create stub notes for major entities so [[wiki-links]] resolve from the start.
For each major character, create a stub with known metadata:
write_note(
title="<Character Name>",
directory="characters/major",
note_type="Character",
tags=["character", "major", "<role>"],
metadata={"role": "<role>", "description": "<brief description>"},
content="""# <Character Name>
## Observations
- [role] <Character's role in the work>
- [appearance] <Key physical description>
## Relations
- associated_with [[<Related Character>]]
- appears_in [[<Key Location>]]"""
)
Identify the work's major entities before you start reading. A good starting inventory:
| Type | Typical Count | What to Include |
|---|---|---|
| Characters (major) | 8-20 | Protagonist, antagonist, key supporting cast |
| Themes | 5-12 | Central concerns the work explores |
| Locations | 4-10 | Primary settings, symbolically significant places |
| Symbols | 4-10 | Recurring objects, images, or motifs with layered meaning |
Stubs don't need to be complete — they give [[wiki-link]] targets and will be enriched during chapter processing.
Obtain the full text and identify chapter/section boundaries. For public domain works, Project Gutenberg is a good source. For copyrighted works, work from a physical or licensed digital copy.
Put the source text inside the project directory, as .txt, and index it once. bm cat
resolves a note identifier, not a filesystem path — it can only reach a file the project
index has observed. A one-time index pass gives the raw text an entity row, after which the
line-range slice works against it:
cp ~/Downloads/moby-dick.txt ~/basic-memory/moby-dick/moby-dick.txt
bm reindex --search -p moby-dick # one pass; the .txt becomes readable
Two constraints that make this the right shape, both worth respecting:
.txt, do not convert it to .md. Basic Memory injects frontmatter into
markdown notes, which shifts every line number by the height of that block — an offset map
built from the original file would then be silently wrong. A .txt is stored verbatim, so
its line numbers stay 1:1 with the file on disk.bm cat answers Error: Entity not found. If you must leave it outside, drop the BM verbs
for the source and use plain shell (sed -n '4200,4890p' <path>) — the notes still get the
BM verbs, only the raw source falls back to the shell.Then build a chapter offset map once, before processing. Scan the text for chapter headings and record the line range of each chapter, then read chapters by range rather than re-reading the whole book into context:
grep -n '^CHAPTER ' ~/basic-memory/moby-dick/moby-dick.txt # heading -> line number
bm cat moby-dick/moby-dick.txt --lines 4200-4890 --plain # one chapter, not the whole text
grep -n here is the shell's grep on a filesystem path (this is the map-building step, and
it needs the file). bm cat then takes the note identifier and returns exactly that slice plus a
lines 4200-4890 of N footer. Spell the identifier project-qualified
(<work>/<work>.txt) so it routes to the right project. bm head moby-dick/moby-dick.txt -n 40 is the cheap
way to eyeball the heading format before writing the grep pattern.
Store the map in the project (a note or a small JSON file) so later batches — and a resumed run after context compaction — do not have to rediscover it. On a long work this is the single largest context saving in the pipeline.
Process ~10 chapters per batch to balance depth with progress. Group by narrative arc or thematic focus:
| Batch | Typical Content |
|---|---|
| 1 | Opening: setting, character introductions, world-building |
| 2-3 | Rising action: conflicts established, relationships develop |
| 4-6 | Middle: complications, turning points, thematic deepening |
| 7-8 | Climax approach: escalation, revelations, crises |
| Final | Climax, resolution, epilogue |
Adjust batch size based on chapter length and density. Short, action-heavy chapters can be batched in larger groups; long, philosophically dense chapters may need smaller batches.
For each chapter:
1. Read the chapter carefully. Read the chapter's line range from the offset map
(bm cat <source>.txt --lines <start>-<end>), not the whole file. Read the actual text —
never work from memory or a summary; textual evidence is the entire point.
2. Create the chapter note:
write_note(
title="Chapter <N> - <Title>",
directory="chapters",
note_type="Chapter",
tags=["chapter", "<arc-phase>"],
metadata={
"chapter_number": <N>,
"pov": "<narrator or POV character>",
"setting": "<primary location>",
"narrative_mode": "<mode>"
},
content="""# Chapter <N> - <Title>
## Observations
- [summary] <1-2 sentence synopsis>
- [event] <Key plot events>
- [tone] <Emotional and stylistic atmosphere>
- [technique] <Notable narrative techniques>
- [quote] "<Significant passage>"
- [significance] <Why this chapter matters to the whole>
- [foreshadowing] <Hints at future events>
## Relations
- features [[<Character>]]
- set_in [[<Location>]]
- explores [[<Theme>]]
- contains [[<Symbol>]]
- employs [[<Literary Device>]]
- follows [[Chapter <N-1> - <Previous Title>]]
- precedes [[Chapter <N+1> - <Next Title>]]"""
)
3. Enrich related entities:
edit_note(
identifier="characters/major/<character-slug>",
operation="insert_after_section",
section="Observations",
content="""- [arc] Ch.<N>: <What happens to this character>
- [quote] "<Attributed quote>" (Ch.<N>)"""
)
append and prepend do not target a section — they write at the end of the note and the
start of the body. Use insert_after_section / insert_before_section to land lines under a
heading; they fail loudly if the heading is missing.
4. Track progress using the memory-tasks skill to create a processing task that survives context compaction.
| Category | What to Look For |
|---|---|
[summary] | 1-2 sentence chapter synopsis |
[event] | Key plot events (actions, revelations, arrivals) |
[tone] | Emotional and stylistic atmosphere |
[technique] | Narrative innovations (POV shifts, structural experiments, genre blending) |
[quote] | Memorable or thematically significant passages |
[significance] | Why this chapter matters to the whole |
[foreshadowing] | Hints at future events |
As each chapter is processed, append observations to relevant entities:
[arc] moments, new [trait] revelations, [quote] attributions[manifestation] in this chapter, [evolution] shifts[appearance] with context, new [interpretation] angles[atmosphere] as described, [significance] in scene[example] from this chapterAfter the structured observations are in place, consider adding interpretive prose to major entity notes. Insert 2-4 paragraphs of critical essay before the Observations section with edit_note(operation="insert_before_section", section="Observations", content=...). This prose should:
The prose adds the interpretive texture that structured observations alone cannot capture.
After all chapters are processed:
Do not re-read every note to decide what is thin. Query for it:
bm find --meta 'note_type=chapter' --fields chapter_number,pov,setting --page-size 200
bm find --meta 'note_type=character' --fields role,status --page-size 200 # who is still a stub
bm find --meta 'chapter_number>100' --fields pov --page-size 200 # late-book POV drift
A field a note never set comes back as a blank cell (null under --json), so rows with
blanks are the work queue. This turns "audit the graph" from a read of every note into one
call per question.
write_note snake-cases note_type (Chapter is stored as chapter), and --meta
type predicates are normalized the same way, so either spelling matches. --page-size 200
is not decoration: without it these return the first 10 rows and the work queue looks ten
items long.
For each major character, write a full [arc] summary observation covering their trajectory across the work.
For each theme, add [evolution] observations tracing how it develops from introduction to resolution.
Add parallels and contrasts_with relations between structurally similar chapters (e.g., mirrored scenes, repeated settings, thematic echoes).
Create synthesis notes in analysis/:
write_note(
title="Narrative Structure",
directory="analysis",
note_type="note",
tags=["analysis", "structure"],
content="""# Narrative Structure
Analysis of the work's narrative architecture.
## Observations
- [structure] <Overall arc description>
- [technique] <Key narrative strategies>
...
## Relations
- analyzes [[<Protagonist>]]
- analyzes [[<Key Character>]]
- explores [[<Central Theme>]]
..."""
)
Recommended analysis notes:
During chapter processing, new minor characters, locations, and symbols will emerge. Create notes for any that appear in 3+ chapters or carry thematic weight.
# Validate each entity type
schema_validate(note_type="Character")
schema_validate(note_type="Theme")
schema_validate(note_type="Chapter")
schema_validate(note_type="Location")
schema_validate(note_type="Symbol")
schema_validate(note_type="LiteraryDevice")
schema_diff(note_type="Character")
# ... for each type
Fix issues found — common fixes:
edit_noteSchema validation proves notes match their shape. These prove the graph is complete:
bm find --meta 'note_type=chapter' --fields chapter_number --page-size 200 # every chapter present?
bm find --meta 'note_type=chapter' --fields pov,setting --page-size 200 # missing context?
bm find /characters --meta 'note_type=character' --fields role --page-size 200 # inventory vs. seed list
A coverage check that pages is not a coverage check. bm find defaults to
--page-size 10, so the un-sized form of the first query "proves" a 138-chapter work has 10
chapters. 200 is the maximum page size; past that, iterate with --page 2, --page 3, … .
Scope a --meta query with the positional path (/characters), never --name — the two
options are mutually exclusive, because the metadata search has no filename glob. The
positional path scopes by the file path a note is indexed under, matched on a directory
boundary: /characters admits characters/major/ahab.md but never characters-cut/. It is
not a permalink match, so a note that pins its own permalink: is still found where its file
lives.
Read the count off the footer, not off the rows you can see. In a terminal every find
result reports page 1 • total 138, and appends • more available (--page) when the page
truncated the answer — that suffix appearing is the check failing, whatever the visible
rows say. The footer is a TTY feature. Piped output without --plain is JSON, which
carries total and has_more; with --plain the rows go to stdout and a has-more notice
goes to stderr. For scripts, read has_more from --json.
For the sequence gap — the failure that a count alone cannot catch — take the numbers from
--json, which carries total, total_is_exact, and has_more:
expected=138; page=1; rows='[]'
while :; do
resp=$(bm find --meta 'note_type=chapter' --fields chapter_number \
--page-size 200 --page $page --project <work> --json)
rows=$(jq -n --argjson acc "$rows" --argjson r "$resp" \
'$acc + [$r.results[] | {title, n: .fields.chapter_number}]')
[ "$(jq -r '.has_more' <<<"$resp")" = "true" ] || break
page=$((page + 1))
done
jq -n --argjson rows "$rows" --argjson expected "$expected" '
([$rows[] | select(.n != null and (.n | tostring | test("^[0-9]+$"))) | .n | tonumber]) as $n
| { total: ($rows | length),
unnumbered: [$rows[] | select(.n == null or (.n | tostring | test("^[0-9]+$") | not))
| .title],
missing: ([range(1; $expected + 1)] - $n),
duplicates: ($n | group_by(.) | map(select(length > 1) | .[0])),
out_of_range: ($n | map(select(. < 1 or . > $expected)) | unique) }'
The loop is not ceremony. --page-size caps at 200, so a single call cannot inventory a work
with more than 200 chapters — and rerunning it with --page 2 replaces the numbers rather
than accumulating them, which reports chapters 1-200 as missing on a corpus that is complete.
Walk until has_more is false and check the union.
--fields returns YAML-native types, so chapter_number: 63 comes back as 63. The
tostring | test(...) | tonumber guard still catches hand-typed values such as "63" or
"12a".
Pass the work's actual chapter count as $expected — deriving the range from the highest
number found lets an incomplete graph pass. With 138 rows numbered 1..137 plus one duplicate,
a max-derived check reports missing: [] while a chapter is genuinely absent: the duplicate
keeps the count right and the missing tail moves the goalpost. The check passes on
unnumbered: [], missing: [], duplicates: [], and out_of_range: [] together, over
the combined pages.
unnumbered is not decoration either. A chapter note that never got a chapter_number comes
back as null, and feeding that straight to tonumber aborts the whole pipeline with null cannot be parsed as a number — so the check crashes on exactly the malformed inventory it
exists to find. Partitioning first turns that into a named row.
out_of_range is not hypothetical: a prologue or epilogue typed as chapter lands at 0 or at
$expected + 1, and without that key the report reads clean — every expected number present,
none repeated — while the inventory holds a note the numbering does not account for. Type
front and back matter as its own note type, or widen $expected deliberately.
A gap in the middle of a batch is the most common processing failure and the easiest to miss by eye; a duplicated chapter number is the second, and it hides the first.
Spot-check bidirectional relations: if Chapter X features [[Character]], does Character have observations referencing Chapter X? Fix gaps.
Orphans are the other half of this check — a note with no inbound or outbound relations is either genuinely isolated or was never linked back into the graph:
bm orphans # entities with no relations in the graph
orphans finds notes with no relations. It does not find a [[target]] that resolves to
nothing — a misspelled or renamed entity name. Check a chapter's links with bm tool build-context memory://chapters/<slug> --depth 1 --project <work> --json and look for relations whose to_entity_id is null; fix the
spelling or add the alias, then re-run the chapter.
Graph quality is relation density, not note count. A pass that adds notes while leaving orphans behind has made the graph worse.
With the graph complete, traverse it to find what the chapter-by-chapter pass could not see:
bm grep "features [[" --page-size 200 --project <work> --json # every chapter's cast, one call
bm find --meta 'note_type=theme' --fields prevalence --page-size 200 # thematic weight
bm grep "doubloon" --page-size 100 --project <work> # every mention of a symbol
"Which characters share the most chapters" is the first line plus a local parse of each
row's content for features [[...]] — one call instead of a read per chapter. Do not
reach for bm tool build-context 'memory://characters/major/*' here: a
wildcard context is capped at 100 related rows across all primaries and returns one primary
row per indexed observation, so it neither enumerates the cast nor walks the web.
build-context on a single note is the right tool for a different question, below.
build-context takes its URL as a positional argument — there is no --url option.
grep matches keywords and returns a page of 10, which quietly truncates "where does this
appear?" — a symbol in 40 chapters comes back as 10. For symbol tracing, raise
--page-size; the meaning shifts you are hunting are usually in the later occurrences, which
the default page would have dropped. --semantic ranks by meaning instead, which answers
"what is this about?" rather than "where does this word appear?".
Two more facts about grep rows. Matching is case-insensitive and note-level: a hit is a
note, not a line, and there is no -n or context. And each row's content is capped at
4000 characters; content_truncated: true marks a cut row, so a long note's tail (such as a
final chapter's follows/precedes relations) may be missing from a grep-driven scan.
When a parse depends on the end of a note, cat that note.
--page-size raises the ceiling, it does not remove it. A symbol in a long work can exceed
even 100, so check whether the last page was full and walk --page 2, --page 3 until it is
not. A truncated symbol search fails the same silent way as an unpaginated find: a plausible
answer, exit 0, and no sign that the tail is missing.
And grep searches your notes, not the source. The <work>.txt is indexed as an entity,
but its body is not in the searchable text, so an occurrence you never carried into a note is
unreachable: a word present only in the source returns total: 0. So this answers "where have I written about the doubloon", not
"where does the doubloon appear in the book". For the latter, search the file itself and use
the chapter offset map to turn a hit into a chapter.
Traversal is where second-order questions get answered — which characters share the most
chapters, which themes converge in the final act, where a symbol's meaning shifts. Capture
what you find as analysis/ notes; those syntheses are the payoff of having built the graph.
This pipeline works for any literary text. Adjust schemas for genre:
| Genre | Schema Adjustments |
|---|---|
| Novel | Base schemas work as-is; add genre-specific Character fields as needed |
| Play | Add Act and Scene schemas; Character gets speaking_lines field |
| Poetry collection | Replace Chapter with Poem; add form, meter, rhyme_scheme fields |
| Non-fiction | Replace Chapter with Section; add Argument, Evidence schemas |
| Short story collection | Add Story schema with narrator, setting, word_count |
| Epic/myth | Add Deity, Prophecy schemas; Location gets mythological_significance |
| Memoir | Character schema gets relationship_to_narrator; add Memory schema |
| Work Length | Batch Size | Estimated Notes |
|---|---|---|
| Novella (~40K words) | 5-10 chapters | ~50-80 |
| Novel (~80K words) | 8-12 chapters | ~100-150 |
| Long novel (~200K+ words) | 10-15 chapters | ~200-300 |
| Series (multiple volumes) | 1 volume at a time | ~200+ per volume |
.txt, index it once, build the chapter offset map once, then read chapters by line range and notes by section. On a long work, whole files landing in context are the largest avoidable cost in the pipeline.--meta predicates and --fields projection. Reading notes to check frontmatter is the mistake this pipeline makes at scale. One way these queries lie quietly: find returns 10 rows unless you pass --page-size.schema_validate after each batch, not just at the end. Catch drift early.[quote] observations with chapter attribution.analysis/ should be written after all chapters are processed, when you have the full picture.OBobra/superpowers298k近 7 天新增 2,631 个 star
MAmattpocock/skills288k近 7 天新增 10,178 个 star
MAmattpocock/skills288k近 7 天新增 10,178 个 star
MAmattpocock/skills288k近 7 天新增 10,178 个 star
AFaffaan-m/ECC278k近 7 天新增 4,046 个 star
AFaffaan-m/ECC278k近 7 天新增 4,046 个 star