# Start Here: What selfgraph Is and How to Read It

> What this repo is, the mental model behind it, the key vocabulary (Capability, PatchProposal, guardrails, sandbox, promote), the fastest read order (README → cli.py → ingest/extract → propose → guardrails → sandbox), and the one constraint to keep in mind: the deterministic extractor is the contract, the LLM pass is optional and additive.

- Repository: yoheinakajima/activegraph-selfgraph
- GitHub: https://github.com/yoheinakajima/activegraph-selfgraph
- Human wiki: https://grok-wiki.com/public/wiki/yoheinakajima-activegraph-selfgraph-41747ef30393
- Complete Markdown: https://grok-wiki.com/public/wiki/yoheinakajima-activegraph-selfgraph-41747ef30393/llms-full.txt

## Source Files

- `README.md`
- `selfgraph/__init__.py`
- `selfgraph/__main__.py`
- `requirements.txt`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [README.md](README.md)
- [selfgraph/__init__.py](selfgraph/__init__.py)
- [selfgraph/__main__.py](selfgraph/__main__.py)
- [selfgraph/cli.py](selfgraph/cli.py)
- [selfgraph/ingest.py](selfgraph/ingest.py)
- [selfgraph/extract.py](selfgraph/extract.py)
- [selfgraph/propose.py](selfgraph/propose.py)
- [selfgraph/guardrails.py](selfgraph/guardrails.py)
- [selfgraph/sandbox.py](selfgraph/sandbox.py)
- [requirements.txt](requirements.txt)
</details>

# Start Here: What selfgraph Is and How to Read It

selfgraph is a minimal ActiveGraph agent that ingests its own source repository and the ActiveGraph runtime, builds a **capability graph** from what it finds, and uses that graph to propose safe, graph-native self-configuration patches for new user goals. It is designed as a working demonstration of a self-modifying agent that is constrained by what it has actually discovered — if a primitive (a Behavior, an EventType, an ObjectType) is not in the graph, it does not appear in a proposal.

This page orients you for your first 30 minutes in the repo. It names the key terms, explains the mental model, lays out the recommended read order, and highlights the one architectural constraint that shapes everything else: **the deterministic extractor is the contract; the LLM pass is optional and purely additive**.

---

## The Mental Model

selfgraph turns a Python repository into a living, queryable graph and then uses that graph as both the knowledge base and the safety boundary for self-modification. There is no hidden prompt that tells the agent what it can do. Instead, the agent reads the graph, proposes changes that reference only nodes already in the graph, and is blocked from introducing anything — a behavior binding, a new ObjectType, a policy — that has not been extracted from real source code or documentation.

```text
┌─────────────────────────────────────────────────────────────────┐
│                     selfgraph pipeline                          │
│                                                                 │
│  [Repo / Package]                                               │
│       │                                                         │
│   ingest.py ──── File + Chunk objects ────► graph.db (SQLite)  │
│       │                                         │               │
│   extract.py ─── Capability, API, Behavior,     │               │
│                  ObjectType, Constraint, ◄──────┘               │
│                  AuthorityRule nodes                            │
│                        │                                        │
│   propose.py ─── PatchProposal (draft) ─────────────────────►  │
│                        │                                        │
│   guardrails.py ─ validate ─► validated | rejected              │
│                        │                                        │
│   sandbox.py ─── fork ─► apply ─► diff ─► (promote)            │
└─────────────────────────────────────────────────────────────────┘
```

Sources: [selfgraph/__init__.py:1-29](selfgraph/__init__.py), [README.md:76-90](README.md)

---

## Key Vocabulary

| Term | Where it lives | What it means |
|------|---------------|---------------|
| **Capability** | Graph node (`type="Capability"`) | A named thing the agent can do, seeded by `_SEED_CAPABILITIES` in `extract.py` and optionally augmented by the LLM pass. Six stable anchors are hardcoded (`ingest-repo`, `extract-capability`, `answer-question`, `propose-patch`, `validate-patch`, `sandbox-apply`). |
| **Behavior** | Graph node (`type="Behavior"`) | An `@behavior` / `@llm_behavior` / `@relation_behavior`-decorated Python function extracted from source. Identified by regex in `extract.py`. A `bind_behavior` change in a proposal must name a Behavior node that actually exists in the graph. |
| **PatchProposal** | Graph node (`type="PatchProposal"`) | The unit of self-modification. Carries `goal`, `rationale`, `changes` (a list of typed change dicts), `evaluation` criteria, and `status`. Created by `propose.py`, validated by `guardrails.py`, applied by `sandbox.py`. |
| **guardrails** | `selfgraph/guardrails.py` | Two-layer validation. Primary: only seven change `kind` values are allowed (`add_object`, `add_relation`, `add_policy`, `add_state_bucket`, `add_task`, `add_evaluation`, `bind_behavior`). Secondary: substring banlist over the whole proposal payload, plus a `_PROTECTED_TYPES` check that blocks adding `AuthorityRule` or `Capability` nodes without explicit approval. |
| **sandbox** | `selfgraph/sandbox.py` | A forked copy of the graph. When a SQLite-backed runtime is active, `Runtime.fork(at_event=...)` is used. Otherwise a structural replay into a fresh `Graph` object is used. Changes are applied to the fork first; only `promote=True` writes them to the live graph. |
| **promote** | `cmd_promote` in `cli.py` | The user-triggered step that re-validates the proposal (with `mutate_status=False`) against the *current* persisted graph state, then calls `sandbox_apply(..., promote=True)` to commit changes. A stale `validated` marker alone is never enough. |
| **AuthorityRule** | Graph node (`type="AuthorityRule"`) | Hardcoded rules in `extract.py` (`no-arbitrary-code`, `no-authority-mutation`, `no-external-side-effects`, `allowed-change-kinds`). Every Capability node carries a `CAPABILITY_REQUIRES_APPROVAL` edge to the `no-authority-mutation` rule. |
| **fallback scaffold** | `propose.py:84-138` | When no extracted Behavior matches the goal, the proposer falls back to a built-in atom/snapshot/`ROLLS_UP_INTO` structure. Proposals that took this path set `used_fallback_scaffold: true` and prefix the rationale with `[FALLBACK]`. |

Sources: [selfgraph/extract.py:96-128](selfgraph/extract.py), [selfgraph/guardrails.py:21-37](selfgraph/guardrails.py), [selfgraph/propose.py:84-104](selfgraph/propose.py), [selfgraph/cli.py:83-101](selfgraph/cli.py)

---

## PatchProposal Lifecycle

The status field on a `PatchProposal` node follows a strict transition path enforced at two call sites (not a state machine class):

```stateDiagram-v2
    [*] --> draft : propose_patch_for()
    draft --> validated : validate_proposal() ok=true
    draft --> rejected : validate_proposal() ok=false
    validated --> applied : sandbox_apply(promote=True)\nafter re-validation
    validated --> rejected : promote re-validation fails
```

- `validate_proposal` flips `draft → validated | rejected` and stamps the report onto the node.
- `cmd_promote` re-runs `validate_proposal(mutate_status=False)` — a pure check — before calling `sandbox_apply(..., promote=True)`.
- `sandbox_apply` refuses to act on any proposal not already in the `validated` state.

Sources: [README.md:136-144](README.md), [selfgraph/cli.py:83-101](selfgraph/cli.py), [selfgraph/sandbox.py:29-35](selfgraph/sandbox.py)

---

## Fastest Read Order

Reading the files in this order gives you the complete mental model in one pass:

### 1. `README.md` — Start here
The README is unusually accurate and self-critical. Read the *Architecture* section and *Limitations* in full. The limitations section (especially the `[FALLBACK]` note and the guardrails caveat) will save you from over-trusting the system's outputs.

### 2. `selfgraph/cli.py` — The entry points
All six commands (`build`, `ask`, `propose`, `promote`, `chat`, `demo`) are plain functions here, 10–20 lines each. Reading them in order is the fastest way to understand what each pipeline stage does and which modules it calls.

Sources: [selfgraph/cli.py:48-140](selfgraph/cli.py)

### 3. `selfgraph/ingest.py` — How source becomes graph nodes
`ingest_paths` walks the filesystem and emits `File` + `Chunk` objects (2000-char chunks, deduped on path + sha256). `ingest_module_docs` introspects a Python package — reflection over `inspect.getmembers` — and renders it as a synthetic `module://` corpus that the extractor reads like markdown.

Sources: [selfgraph/ingest.py:40-80](selfgraph/ingest.py), [selfgraph/ingest.py:125-170](selfgraph/ingest.py)

### 4. `selfgraph/extract.py` — How chunks become capability nodes
The deterministic pass runs first, always. It uses six regex patterns over chunk text: `@behavior` decorators, `@tool` registrations, `## def`/`## class` headings from synthetic module files, `ObjectType(name=...)` constructor calls, markdown code blocks (as Example nodes), and `must`/`must not` sentences (as Constraint nodes). The optional LLM pass runs after, on markdown chunks only, and adds `Capability` and `Constraint` nodes. It cannot remove or alter nodes from the deterministic pass.

Sources: [selfgraph/extract.py:36-91](selfgraph/extract.py), [selfgraph/extract.py:131-162](selfgraph/extract.py)

### 5. `selfgraph/propose.py` — How goals become patch proposals
`propose_patch_for` always does four things: (1) adds an `ObjectType` state bucket for the goal, (2) adds a `Task` object, (3) tries to bind existing `Behavior` nodes whose `on=` event lists overlap goal words — and falls back to the atom/snapshot scaffold if nothing matches, (4) adds a scoped `Policy` and `Evaluation` criteria. The resulting `PatchProposal` object is wired with `PATCH_PROPOSES` edges to the `Capability` nodes it uses.

Sources: [selfgraph/propose.py:31-215](selfgraph/propose.py)

### 6. `selfgraph/guardrails.py` + `selfgraph/sandbox.py` — Safety and apply
`validate_proposal` enforces the allowed change `kind` list, the banned-token substring scan, `_PROTECTED_TYPES`, and the `bind_behavior` known-behavior check. `sandbox_apply` builds a forked graph (SQLite fork preferred, structural replay fallback), applies the changes there, runs a smoke `TestEvent`, diffs against the original, and optionally promotes.

Sources: [selfgraph/guardrails.py:45-78](selfgraph/guardrails.py), [selfgraph/sandbox.py:16-73](selfgraph/sandbox.py)

---

## The One Constraint to Keep in Mind

> **The deterministic extractor is the contract; the LLM pass is optional and additive.**

This shapes every other design decision:

- Running `python -m selfgraph build .` without an `ANTHROPIC_API_KEY` produces the same graph shape on every machine. The paper harness actively refuses to run while `ANTHROPIC_API_KEY` is set unless `SELFGRAPH_HARNESS_ALLOW_LLM=1` is also set.
- When the LLM pass is enabled, it only calls the Anthropic API over markdown chunks, parses the JSON response, and adds `Capability` / `Constraint` nodes that are not already in the graph. It cannot remove or replace deterministically extracted nodes.
- The `SELFGRAPH_OBJECTTYPE_MATCH` environment variable (`literal` or `relaxed`, default `relaxed`) controls which regex patterns the deterministic pass uses to detect `ObjectType` names. This is how the paper's before/after A/B comparison is cold-reproducible without touching code.

Sources: [selfgraph/extract.py:63-91](selfgraph/extract.py), [selfgraph/extract.py:334-389](selfgraph/extract.py), [requirements.txt:1-8](requirements.txt), [README.md:27-30](README.md)

---

## Node Types and Relations at a Glance

**Node types** emitted by the pipeline:

| Stage | Node types created |
|-------|--------------------|
| `ingest.py` | `File`, `Chunk` |
| `extract.py` (deterministic seed) | `Capability`, `AuthorityRule` |
| `extract.py` (chunk scan) | `API`, `Behavior`, `EventType`, `ObjectType`, `Example`, `Constraint` |
| `extract.py` (LLM pass, optional) | `Capability`, `Constraint` |
| `propose.py` | `PatchProposal`, `Evaluation`, `Task` (via applied changes: `Policy`, `BehaviorBinding`) |

**Key relation types** wiring the graph:

| Relation | Source → Target |
|----------|----------------|
| `FILE_HAS_CHUNK` | File → Chunk |
| `BEHAVIOR_SUBSCRIBES_TO` | Behavior → EventType |
| `EXAMPLE_DEMONSTRATES` | Behavior → Chunk |
| `CAPABILITY_REQUIRES_APPROVAL` | Capability → AuthorityRule |
| `API_CREATES / API_READS / API_WRITES` | Capability → API |
| `PATCH_PROPOSES` | PatchProposal → Capability |
| `PATCH_MODIFIES` | PatchProposal → ObjectType |
| `GROUNDED_IN` | ObjectType (new atom) → ObjectType (existing) |
| `ROLLS_UP_INTO` | ObjectType (atom) → ObjectType (snapshot) |

Sources: [README.md:94-105](README.md), [selfgraph/extract.py:207-303](selfgraph/extract.py)

---

## Quick-Start Command Reference

```bash
# Install (Python 3.11+)
pip install -r requirements.txt          # activegraph==1.0.5.post2; anthropic optional

# Three-step scripted demo
python demo.py

# Or manually, state persists to .selfgraph/graph.db
python -m selfgraph build .              # ingest repo + activegraph package + extract
python -m selfgraph ask "what can you do?"
python -m selfgraph propose "track inbound emails from a vendor"
python -m selfgraph promote PatchProposal#NNN   # re-validates before applying
python -m selfgraph chat                 # interactive REPL
```

The state file `.selfgraph/graph.db` is a SQLite event log. Delete it to start fresh. `build` always wipes and recreates it (`create=True` in `_open`). All other commands call `Runtime.load(_DB_PATH)` to replay the event log and reconstruct the graph.

Sources: [selfgraph/cli.py:30-57](selfgraph/cli.py), [README.md:22-49](README.md)

---

## What selfgraph Intentionally Cannot Do

Understanding the hard limits saves debugging time:

- **No new Python functions.** `bind_behavior` can only reference behavior names already discovered by the extractor. The agent cannot author code.
- **No external side effects.** No HTTP, shell, or file writes outside the SQLite event store. `_BANNED_TOKENS` in `guardrails.py` blocks `subprocess`, `os.system`, `eval(`, `exec(`, `requests.`, `socket.`, and a dozen more.
- **No elevation of authority.** Patches cannot add `AuthorityRule` or `Capability` nodes without explicit `approved_by` argument. Policies cannot declare `can_approve`.
- **No multi-step planning.** One `PatchProposal` per goal; proposals are not chained.
- **No semantic question answering.** `query.py` is keyword-overlap retrieval over node data fields, not embedding search or LLM-based reasoning.

The README labels these as "v1, on purpose" limitations and explains the rationale for each. The safety guarantee rests on the structural constraint (only seven allowed change kinds, none of which can introduce code) rather than the substring banlist, which the README explicitly calls demo-grade and easy to evade on docs that merely *mention* banned tokens.

Sources: [README.md:159-194](README.md), [selfgraph/guardrails.py:21-37](selfgraph/guardrails.py)
