GhostPaw

@ghostpaw/souls 0.3.0

Change your agent's prompt when the evidence agrees.

souls keeps an agent's identity as a short essence plus traits, and every trait carries the evidence that earned it. Observations pile up as shards, the soul is ready for a change only when they come from two independent sources over at least a day, and every level-up can be reverted.

As a library

As a local MCP server

Replay the run Read the source on GitHub

Free and MIT licensed for Node.js 24 or newer, with zero runtime dependencies. The MCP server keeps identity and evidence in ~/.ghostpaw/souls.db on your machine.

A ghost-wolf head drawn in cyan light filaments, small glass shards floating toward it and a violet crystal forming beside its muzzle.

0runtime dependencies

One reviewer soul, six calls

Three observations from the same place are not enough. A fourth from somewhere else is.

This is the Reviewer soul from the README quick start, replayed from a real run: we made these calls against souls 0.3.0 on 2026-10-24 with a fixed clock, starting from two traits and two session shards, one of them two days old. Every result below is what came back. Run the steps in order.

Reviewer soul

  • Level1
  • Trait 1Read the PR description before the diff.
  • Trait 2Say what breaks before naming style nits.
  • Pending shards2
  • Sources1 of 2
  • Readyno

read.crystallizationReadiness(db)

  1. 01 A shard from a review session

    A shard is one observation, tagged with where it came from.

    write.dropShard(db, {
      content: 'Flagged a missing migration after reading the linked issue.',
      source: 'session',
      soulIds: [reviewer.id],
    });
    // { shard: { id: 3, status: 'pending', ... }, crystallizationTriggers: [] }
    
    read.crystallizationReadiness(db)
    // []

    Three pending shards, all from sessions. The soul is not ready.

  2. 02 A shard from CI

    Same soul, a second and independent source.

    write.dropShard(db, {
      content: 'CI check: the reviewer approved a PR whose description said tests were pending.',
      source: 'ci',
      soulIds: [reviewer.id],
    });
    // { shard: { id: 4, ... }, crystallizationTriggers: [1] }
    
    read.crystallizationReadiness(db)
    // [{ soulId: 1, pendingCount: 4, sourceDiversity: 2, ageSpreadDays: 2, clusterCount: 4, ... }]

    Two sources over two days. The soul is ready for refinement.

  3. 03 Revise trait 1

    The new wording and the reason for it are stored together.

    write.reviseTrait(db, trait1.id, {
      principle: 'Read the PR description and its linked issue before the diff.',
      provenance: 'Four shards across session and CI over two days: '
        + 'the linked issue held the missing context twice.',
    });
    // { id: 1, principle: 'Read the PR description and its linked issue before the diff.',
    //   provenance: 'Four shards across session and CI over two days: ...', status: 'active' }

    The trait changes and its provenance travels with it.

  4. 04 Level up and forget trait 1

    Every active trait has to be merged, promoted or carried.

    const plan = {
      newEssence: '...',
      consolidations: [],
      promotedTraitIds: [trait2.id],
      carriedTraitIds: [],
    };
    read.validateLevelUpPlan(db, reviewer.id, plan);
    write.levelUp(db, reviewer.id, plan);
    // { valid: false, error: { missingTraitIds: [1], duplicateTraitIds: [], invalidTraitIds: [] } }
    
    // SoulsValidationError
    // Level-up plan is invalid. Missing: [1]. Duplicates: []. Invalid: [].

    Refused. The plan dropped trait 1, so nothing changed.

  5. 05 Level up properly

    Trait 2 moves into the essence; trait 1 is carried forward.

    write.levelUp(db, reviewer.id, {
      newEssence: 'I review code by reading for intent before scanning for bugs, '
        + 'and I say what breaks before I mention style.',
      consolidations: [],
      promotedTraitIds: [trait2.id],
      carriedTraitIds: [trait1.id],
    });
    read.renderSoul(db, reviewer.id);
    # Reviewer
    *Code review specialist*
    
    I review code by reading for intent before scanning for bugs, and I say what breaks before I mention style.
    
    ## Traits
    - Read the PR description and its linked issue before the diff.

    Level 2. This block is what goes into the system prompt, and it stays the same until the next change.

  6. 06 Revert the level-up

    Say the new essence reads worse in practice.

    write.revertLevelUp(db, reviewer.id);
    read.renderSoul(db, reviewer.id);
    # Reviewer
    *Code review specialist*
    
    I review code by reading for intent before scanning for bugs.
    
    ## Traits
    - Read the PR description and its linked issue before the diff.
    - Say what breaks before naming style nits.

    Back to level 1 with both traits. Trait 1 keeps its revised text, because that revision came before the level-up.

The gate rules come from the library: at least two independent sources and at least a day between the oldest and newest pending shard.

All four shards are still pending after the level-up. Using them up is a separate decision your code makes.

What a soul is made of

Stacked plates of glass drawn in cyan light, a violet beam rising through them and small shards drifting upward.

A soul is one identity, and it is not a prompt template.

Essence
A few sentences in the first person about how the agent thinks and judges.
Trait
One principle with its provenance, the evidence that it was earned.
Shard
One observation from any source, attributed to one or more souls. Shards pile up quietly until the gate opens.
Level-up
Merge overlapping traits, promote absorbed ones into the essence, carry the rest. The level goes up by one, and revertLevelUp goes back.
Rendered block
renderSoul prints essence and active traits as markdown for your system prompt. It only changes when the soul does.
Mentor
One built-in soul, tuned to guide the refinement steps. soul.renderSoulsSoulPromptFoundation() returns it.

Seven tools for agents: search_souls, review_souls, inspect_souls_item, observe_soul, refine_soul, level_up_soul and manage_soul.

If a prompt that edits itself worries you

People have watched agents that rewrite their own instructions do strange things, and deleting SOUL.md just sends an agent back to its default. Both are reasonable things to worry about.

  • souls does not call a model. Your code or your agent proposes a change, and souls checks it and keeps the history.
  • A level-up that drops an active trait is refused before anything is written.
  • Every level-up can be reverted, and every trait keeps its provenance.
  • The identity and its full history sit in SQLite next to your other data, so going back means a call, not a restore from backup.
  • Node.js 24 or newer, because it uses the built-in node:sqlite.

What it costs to add

Runtime dependencies
0
Node.js
24 or newer (node:sqlite)
Size of dist/index.js, gzip
31.4 KB
Import time
about 3.8 ms
Tests
532 passing
Line coverage
93.4%
Agent tools
7

Measured 2026-10-24 on the maintainer's machine with Node.js 24.14.0 against souls 0.3.0: size of the published dist/index.js at gzip -9, median of 11 cold imports, tests and coverage from the v0.3.0 tag.

Start from the README

import { DatabaseSync } from 'node:sqlite';
import { initSoulsTables, read, write } from '@ghostpaw/souls';

const db = new DatabaseSync(':memory:');
initSoulsTables(db);

const soul = write.createSoul(db, {
  name: 'Reviewer',
  essence: 'I review code by reading for intent before scanning for bugs.',
  description: 'Code review specialist',
});

write.addTrait(db, soul.id, {
  principle: 'Read the PR description before the diff.',
  provenance: 'Reviews that started with code missed architectural intent 3 of 4 times.',
});

write.dropShard(db, {
  content: 'Reviewer caught a naming inconsistency by reading the RFC first.',
  source: 'session',
  soulIds: [soul.id],
});

const rendered = read.renderSoul(db, soul.id);

To run it as an MCP server, add npx -y @ghostpaw/souls mcp to your client. npx -y @ghostpaw/souls skill prints the Agent Skill that ships in the package.