memory-literary-analysis — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited memory-literary-analysis (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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 notesWriting goes through bm_write and bm_edit. MCP reads use bm_read, bm_find, bm_search, and bm_list. The CLI keeps its POSIX verbs; the table and shell examples below are CLI commands (prefix each verb with bm):
| 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 --fieldsprojection are for — one call answers what a read-per-note loop would cost.
slice the output: the full note is still fetched, then cut down before it is returned. What they save is context, not I/O — a long chapter or a full source text costs you the tokens of the relevant part, not of the whole file.
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:
needs --page-size 200 (the maximum) — see Coverage Checks.
--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/.
For MCP, use bm_read(identifier="<note>", section="Observations") or start_line/end_line for slices, bm_find(filters={"type": "chapter"}, fields=["chapter_number", "pov"]) for metadata, and bm_search(query="doubloon", lines=2) for matching lines. bm_list lists one folder level; bm_find recurses. MCP list/find/search pages cap at 100; follow has_more through every page. CLI page sizes and flags below are separate from the MCP shapes.
bm_project(command="create", name="<work-name>", 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.
#### Character Schema
bm_write(
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.
#### Theme Schema
bm_write(
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"""
)#### Chapter Schema
bm_write(
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"""
)#### Location Schema
bm_write(
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"""
)#### Symbol Schema
bm_write(
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"""
)#### LiteraryDevice Schema
bm_write(
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 trackerBefore 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:
bm_write(
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 readableTwo constraints that make this the right shape, both worth respecting:
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 textgrep -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:
bm_write(
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:
bm_edit(
identifier="characters/major/<character-slug>",
command="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 bm_edit(command="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 driftA 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.
bm_write 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/:
bm_write(
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
bm_schema(command="validate", note_type="Character")
bm_schema(command="validate", note_type="Theme")
bm_schema(command="validate", note_type="Chapter")
bm_schema(command="validate", note_type="Location")
bm_schema(command="validate", note_type="Symbol")
bm_schema(command="validate", note_type="LiteraryDevice")bm_schema(command="diff", note_type="Character")
# ... for each typeFix issues found — common fixes:
bm_editSchema 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 listA 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 graphorphans 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.bm_schema(command="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.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.