Skip to main content
Glama
vietqtran

decision-graph

by vietqtran
README.md
# decision-graph

> **English** · [Tiếng Việt](README.vi.md)

[![CI](https://github.com/vietqtran/decision-graph/actions/workflows/ci.yml/badge.svg)](https://github.com/vietqtran/decision-graph/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)

**Decision memory for codebases worked on by AI agents.**

A code graph tells you *what* the code does. `decision-graph` tells you *why it is that way* — when a business rule appeared, who decided it, what alternatives were rejected, and whether the decision still holds.

`git blame` gives you a commit message. It does not tell you that a customer's CTO asked for a second approval tier in a meeting, that a separate module was considered and rejected as unmergeable, or that the threshold was lowered six months later by someone else.

Language- and framework-agnostic. Works in any repo.

## Why

The pain is sharpest when one base product is extended per customer — but it shows up anywhere business logic accumulates:

- An agent "improves" a rule that was deliberately written that way for one tenant.
- Nobody remembers whether an odd-looking branch is a bug or a requirement.
- The same rejected approach gets proposed again every few months.
- Decisions live in Slack threads, ticket comments, and people's heads.

AI agents make this worse, because they have no implicit memory of the last six months of meetings — but they *will* follow a written record if one exists.

## Design principles

- **Markdown is the source of truth.** One `.md` file per decision, in git, reviewable in a PR.
- **SQLite is only an index.** Delete `decisions/_index/` and rebuild any time.
- **Agents may not invent `decided_by`.** An agent can only create a `draft`; promoting it to `active` requires a human.
- **Recording is a side effect of coding**, not a chore to remember — hooks prompt at the right moment.
- **Detection reads git, not tool events.** Edits made with `sed`, heredocs, `git apply`, or a plain editor are caught just like `Edit`/`Write` tool calls.

## Install

Not on PyPI yet — install straight from GitHub:

```bash
uv tool install "decision-graph[mcp] @ git+https://github.com/vietqtran/decision-graph"
```

<details>
<summary>Other ways</summary>

```bash
# pipx
pipx install "decision-graph[mcp] @ git+https://github.com/vietqtran/decision-graph"

# one-off, no install
uvx --from "git+https://github.com/vietqtran/decision-graph" decision-graph --help

# local development
git clone https://github.com/vietqtran/decision-graph && cd decision-graph
uv venv && uv pip install -e ".[mcp]"
```

</details>

Requires Python 3.10+ and a SQLite build with FTS5 (standard on macOS, Debian/Ubuntu, and the official Python images).

## Quick start

```bash
cd /path/to/your/repo
decision-graph init

decision-graph add --scope acme --module deals/approval \
  --title "Second approval tier for deals over 500M" \
  --file src/approval.py --tag override-base --stdin < body.md

# a human confirms who decided — the agent cannot do this step
decision-graph confirm acme-2026-08-26-second-approval-tier --by "Jane Doe (CTO, Acme)"

decision-graph search "two-tier approval" --scope acme
decision-graph history src/approval.py   # every decision that touched this file
decision-graph overlay acme              # how acme differs from base, and why
```

## Core concept: `scope`

`scope` is the axis that separates contexts — a customer, a product line, a team, or `_base` for decisions that apply everywhere.

Filtering by scope happens *before* full-text ranking, which is what stops one tenant's rules from bleeding into answers about another. In a repo serving many customers, this is the single most important field.

## Layout

```
decisions/
  _template.md
  _base/                          # applies to every scope
  acme/2026-08-26-approval.md
  viettel/2026-05-20-inventory.md
  _index/decisions.db             # generated — gitignored
.decision-graph.yml               # per-repo watch/ignore patterns
```

A record's frontmatter:

```yaml
id: acme-2026-08-26-second-approval-tier
scope: acme
module: deals/approval
title: "Second approval tier for deals over 500M"
status: active                 # draft | active | superseded | deprecated
lifecycle_stage: maintenance   # design | dev | uat | golive | maintenance
supersedes: acme-2026-03-12-single-tier
decided_by: "Jane Doe (CTO, Acme)"
requested_by: "John Smith (PM)"
decided_at: 2026-08-26
linked_files: [src/approval.py]
linked_commit: 8f3a2c1
session_id: abc-123            # agent session that produced the change
ticket: JIRA-482
tags: [approval, override-base]
```

The body follows `decisions/_template.md`: Context / Alternatives considered / Decision / Impact / Open risks.

**Alternatives considered matters more to agents than to humans** — it is what stops an agent from re-proposing the approach that was already rejected.

`supersedes` chains records instead of deleting them, so the audit trail survives.

## Search

Metadata filters first (`scope`, `module`, `status`, `lifecycle_stage`, `file`, `tag`), then SQLite FTS5 ranking. Diacritics are folded, so `duyet don hang` matches `Duyệt đơn hàng`.

```bash
decision-graph search "approval"                    # active decisions only, by default
decision-graph search --scope acme --status any
decision-graph search --file src/approval.py
decision-graph search "pricing" --tag override-base --json
```

## Agent integration

### Which trigger to use

| Situation | Trigger |
|---|---|
| Claude Code runs on the **same machine** as the repo | Claude Code hooks — can block, strongest |
| Claude Code runs **elsewhere** (SSH / VM / remote) | Git hook — reminds, cannot block |
| Humans committing without an agent | Git hook + `check` in CI |

Installing both is fine; each goes quiet once a decision is recorded.

### Claude Code hooks

```bash
decision-graph hooks install --target /path/to/repo
```

- `PostToolUse(Edit|Write|MultiEdit)` records which files a session touched.
- `Stop` inspects git plus that record. If business-relevant files changed and no decision was written, it returns `decision: "block"` with instructions — the agent must act instead of finishing.

The agent gets exactly two ways out:

```bash
decision-graph skip --session <id> --reason "renamed variables only"
decision-graph add ... --session <id>   # then ask the human, then confirm
```

`stop_hook_active` is honoured, so this never loops.

> **Install hooks where Claude Code actually runs**, not where the code lives. If you run Claude Code on a VM and only proxy shell commands to the machine holding the repo, that machine's `.claude/settings.json` is never read — use the git hook instead.

### Git hook

```bash
decision-graph hooks install --git --target /path/to/repo
```

Installs `.git/hooks/post-commit` calling `decision-graph remind`. It never blocks a commit. Because agents run git through their shell and read stdout, the reminder lands in the agent's context anyway.

It stays silent once a decision links to that commit.

### MCP server

One entry, declared once at user level — the server follows whichever repo the session has open, so there is no path to keep in sync:

```json
{
  "mcpServers": {
    "decision-graph": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/vietqtran/decision-graph",
               "decision-graph-mcp"]
    }
  }
}
```

It asks the client which directory the session is working in (MCP roots), falling back to the working directory. Repos without a `decisions/` directory are skipped rather than guessed at. Add `--path /path/to/repo` only to pin the server to one repo.

Tools: `search_decisions`, `get_decision`, `get_decision_history`, `get_decision_chain`, `get_overlay_map`, `list_scopes`, `add_decision`.

See [docs/MCP.md](docs/MCP.md).

## Filtering noise

Not every commit is a business decision. Typos and pure refactors should not generate records.

Defaults ignore tests, lockfiles, `node_modules`, build output, coverage reports, assets, and i18n files. Narrow further per repo:

```yaml
# .decision-graph.yml
watch:
  - "src/domain/**"
  - "app/services/**"
```

An empty `watch` means everything not in `ignore` counts.

## CI

```bash
decision-graph check --git
```

Emits `{"needs_decision": bool, "watched_files": [...], ...}` — wire it into a PR warning.

## Documentation

- [CLI reference](docs/CLI.md)
- [Architecture](docs/ARCHITECTURE.md)
- [MCP server](docs/MCP.md)
- [Troubleshooting](docs/TROUBLESHOOTING.md)
- [Contributing](CONTRIBUTING.md)

## Development

```bash
uv venv && uv pip install -e ".[mcp]"
.venv/bin/python -m unittest discover -s tests
```

No runtime dependency beyond PyYAML; `mcp` is an optional extra.

## Prior art

`decision-graph` is an [ADR](https://adr.github.io/) variant. ADRs record *architectural* decisions for humans; this records *business* decisions, per tenant, in a form agents can query — with automatic capture and a human confirmation gate.

## License

[MIT](LICENSE)