From 5955e363600445cd17a368eac1f15d3dc59f3d64 Mon Sep 17 00:00:00 2001 From: anusbutt Date: Thu, 20 Aug 2026 22:46:36 +0500 Subject: [PATCH 1/2] feat: add Kilo, Windsurf, and Gemini CLI adapters Register kilo, windsurf, and geminicli in the data-driven adapter registry, each with a repo-local canonical skill and a marked guidance reminder. The AgentAdapter shape is unchanged; the CLI grammar, --integrate all, and removal machinery derive from AGENT_IDS with no command-logic changes. --- AGENTS.md | 3 +- CHANGELOG.md | 12 ++ README.md | 12 +- docs/agent-integrations.md | 3 + src/lib/agent-adapters.ts | 30 +++++ tests/e2e/init.test.ts | 78 +++++++++++++ tests/integration/agent-integrations.test.ts | 113 +++++++++++++++++++ tests/unit/agent-adapters.test.ts | 63 ++++++++++- 8 files changed, 308 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c5ca183..9c9de22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,8 @@ transcript store. - `templates/graph/SCHEMA.md` is the data contract; `templates/SKILL.md` is vendor-neutral agent guidance; `templates/pre-commit` is the hook wrapper. - `src/lib/agent-adapters.ts` defines the closed, data-driven adapter registry - (Codex, Claude Code, Cursor, OpenCode). Adding an adapter is a single registry entry; + (Codex, Claude Code, Cursor, OpenCode, Kilo Code, Windsurf, Gemini CLI). Adding an + adapter is a single registry entry; the CLI grammar and `--integrate all` derive from it. Adapters may share a guidance file (Codex and OpenCode both use `AGENTS.md`): each owns one marked block, sibling blocks from registered adapters are allowed when properly paired, and unknown or diff --git a/CHANGELOG.md b/CHANGELOG.md index e67ddbe..82e6c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,18 @@ versioning; while the package is below 1.0, minor releases may change public beh `.opencode/skills/graphkeeper/SKILL.md`, the marked reminder in `AGENTS.md`, and the `graphkeeper` invocation. It participates in `--integrate all`, `--dry-run`, and conservative `integrate remove`. +- A Kilo Code adapter registered as `--integrate kilo` with the canonical skill at + `.kilo/skills/graphkeeper/SKILL.md`, the marked reminder at + `.kilo/rules/graphkeeper.md`, and the `@graphkeeper` invocation. It participates in + `--integrate all`, `--dry-run`, and conservative `integrate remove`. +- A Windsurf adapter registered as `--integrate windsurf` with the canonical skill at + `.windsurf/skills/graphkeeper/SKILL.md`, the marked reminder at + `.windsurf/rules/graphkeeper.md`, and the `@graphkeeper` invocation. It participates in + `--integrate all`, `--dry-run`, and conservative `integrate remove`. +- A Gemini CLI adapter registered as `--integrate geminicli` with the canonical skill at + `.gemini/skills/graphkeeper/SKILL.md`, the marked reminder in `GEMINI.md`, and the + `@graphkeeper` invocation. It participates in `--integrate all`, `--dry-run`, and + conservative `integrate remove`. ### Changed diff --git a/README.md b/README.md index c3b3acb..dbc1a18 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,19 @@ Review the displayed plan and confirm it. `init` creates the JSON graph, `eviden the canonical validator, a pre-commit hook, and the repository-scoped Codex skill. `--integrate codex` adds the Codex reminder to `AGENTS.md`; `--integrate claude` adds the Claude skill and reminder; `--integrate cursor` adds the Cursor skill and rule; -`--integrate opencode` adds the OpenCode skill and reminder; +`--integrate opencode` adds the OpenCode skill and reminder; `--integrate kilo` adds +the Kilo Code skill and rule; `--integrate windsurf` adds the Windsurf skill and rule; +`--integrate geminicli` adds the Gemini CLI skill and reminder; `--integrate all` installs every registered adapter. Codex uses `.agents/skills/graphkeeper/SKILL.md`, `AGENTS.md`, and `$graphkeeper`. Claude Code uses `.claude/skills/graphkeeper/SKILL.md`, `CLAUDE.md`, and `/graphkeeper`. Cursor uses `.cursor/skills/graphkeeper/SKILL.md`, `.cursor/rules/graphkeeper.md`, and `@graphkeeper`. OpenCode uses -`.opencode/skills/graphkeeper/SKILL.md`, `AGENTS.md`, and `graphkeeper`. All skills +`.opencode/skills/graphkeeper/SKILL.md`, `AGENTS.md`, and `graphkeeper`. Kilo Code uses +`.kilo/skills/graphkeeper/SKILL.md`, `.kilo/rules/graphkeeper.md`, and `@graphkeeper`. +Windsurf uses `.windsurf/skills/graphkeeper/SKILL.md`, `.windsurf/rules/graphkeeper.md`, +and `@graphkeeper`. Gemini CLI uses `.gemini/skills/graphkeeper/SKILL.md`, `GEMINI.md`, +and `@graphkeeper`. All skills are generated from the same `templates/SKILL.md`. The set of adapters is a closed data-driven registry in `src/lib/agent-adapters.ts`; `graphkeeper --help` lists the installed adapters. Adapters that share a guidance file (Codex and OpenCode both use @@ -138,7 +144,7 @@ The older claim remains in history and is marked as superseded. Reviewers can fo | Command | Role | |---|---| -| `graphkeeper init [--force] [--integrate ]... [--yes] [--dry-run]` | Scaffold safely and optionally install explicit agent adapters (currently `codex`, `claude`, `cursor`, `opencode`). Distinct `--integrate` flags may repeat; `all` must stand alone. `--yes --dry-run` is accepted as a harmless dry run. | +| `graphkeeper init [--force] [--integrate ]... [--yes] [--dry-run]` | Scaffold safely and optionally install explicit agent adapters (currently `codex`, `claude`, `cursor`, `opencode`, `kilo`, `windsurf`, `geminicli`). Distinct `--integrate` flags may repeat; `all` must stand alone. `--yes --dry-run` is accepted as a harmless dry run. | | `graphkeeper integrate remove [--yes] [--dry-run]` | Remove only recognizable GraphKeeper-owned material for one adapter. Modified skills and unexpected supporting files are preserved for manual review. | | `graphkeeper check` | Run the same fast schema, append-only history, and committed-evidence protection checks used by the Git hook. | | `graphkeeper query ` | Resolve an exact ID or unique alias and print active claims with provenance. It does not read evidence contents. | diff --git a/docs/agent-integrations.md b/docs/agent-integrations.md index e582c56..c2fdcd1 100644 --- a/docs/agent-integrations.md +++ b/docs/agent-integrations.md @@ -8,6 +8,9 @@ GraphKeeper v1 supports the following explicit internal adapters: | `claude` | `.claude/skills/graphkeeper/SKILL.md` | `CLAUDE.md` | `/graphkeeper` | | `cursor` | `.cursor/skills/graphkeeper/SKILL.md` | `.cursor/rules/graphkeeper.md` | `@graphkeeper` | | `opencode` | `.opencode/skills/graphkeeper/SKILL.md` | `AGENTS.md` | `graphkeeper` | +| `kilo` | `.kilo/skills/graphkeeper/SKILL.md` | `.kilo/rules/graphkeeper.md` | `@graphkeeper` | +| `windsurf` | `.windsurf/skills/graphkeeper/SKILL.md` | `.windsurf/rules/graphkeeper.md` | `@graphkeeper` | +| `geminicli` | `.gemini/skills/graphkeeper/SKILL.md` | `GEMINI.md` | `@graphkeeper` | Some adapters (for example Codex and OpenCode) share `AGENTS.md` as their guidance file. GraphKeeper supports this: each adapter owns exactly one marked block, blocks diff --git a/src/lib/agent-adapters.ts b/src/lib/agent-adapters.ts index d8abf9d..b36c3f6 100644 --- a/src/lib/agent-adapters.ts +++ b/src/lib/agent-adapters.ts @@ -61,6 +61,36 @@ const adapters = [ endMarker: '', postInstallNote: 'Restart opencode if .opencode/skills did not exist when the current session began.', }, + { + id: 'kilo', + displayName: 'Kilo Code', + skillTarget: '.kilo/skills/graphkeeper/SKILL.md', + guidanceTarget: '.kilo/rules/graphkeeper.md', + invocation: '`@graphkeeper`', + startMarker: '', + endMarker: '', + postInstallNote: 'Restart Kilo Code if .kilo/skills did not exist when the current session began.', + }, + { + id: 'windsurf', + displayName: 'Windsurf', + skillTarget: '.windsurf/skills/graphkeeper/SKILL.md', + guidanceTarget: '.windsurf/rules/graphkeeper.md', + invocation: '`@graphkeeper`', + startMarker: '', + endMarker: '', + postInstallNote: 'Restart Windsurf if .windsurf/skills did not exist when the current session began.', + }, + { + id: 'geminicli', + displayName: 'Gemini CLI', + skillTarget: '.gemini/skills/graphkeeper/SKILL.md', + guidanceTarget: 'GEMINI.md', + invocation: '`@graphkeeper`', + startMarker: '', + endMarker: '', + postInstallNote: 'Restart Gemini CLI if .gemini/skills did not exist when the current session began.', + }, ] as const; export type AgentId = (typeof adapters)[number]['id']; diff --git a/tests/e2e/init.test.ts b/tests/e2e/init.test.ts index 6b5d9df..a198d2a 100644 --- a/tests/e2e/init.test.ts +++ b/tests/e2e/init.test.ts @@ -125,6 +125,69 @@ test('explicit OpenCode integration creates the managed AGENTS.md block through } }); +test('explicit Kilo integration creates the managed rules block and skill through the CLI', async () => { + const fixture = await createRepositoryFixture(); + try { + const result = await runInit(fixture.root, ['--integrate', 'kilo', '--yes']); + assert.equal(result.exitCode, EXIT_SUCCESS, result.stderr); + assert.match(result.stdout, /CREATE \.kilo\/rules\/graphkeeper\.md/); + const rules = await readFile( + join(fixture.root, '.kilo', 'rules', 'graphkeeper.md'), + 'utf8', + ); + assert.match(rules, //); + assert.match(rules, /invoke `@graphkeeper`/); + assert.equal((rules.match(/graphkeeper:kilo:start/g) ?? []).length, 1); + assert.match( + await readFile(join(fixture.root, '.kilo', 'skills', 'graphkeeper', 'SKILL.md'), 'utf8'), + /^---\nname: graphkeeper\n/, + ); + } finally { + await fixture.cleanup(); + } +}); + +test('explicit Windsurf integration creates the managed rules block and skill through the CLI', async () => { + const fixture = await createRepositoryFixture(); + try { + const result = await runInit(fixture.root, ['--integrate', 'windsurf', '--yes']); + assert.equal(result.exitCode, EXIT_SUCCESS, result.stderr); + assert.match(result.stdout, /CREATE \.windsurf\/rules\/graphkeeper\.md/); + const rules = await readFile( + join(fixture.root, '.windsurf', 'rules', 'graphkeeper.md'), + 'utf8', + ); + assert.match(rules, //); + assert.match(rules, /invoke `@graphkeeper`/); + assert.equal((rules.match(/graphkeeper:windsurf:start/g) ?? []).length, 1); + assert.match( + await readFile(join(fixture.root, '.windsurf', 'skills', 'graphkeeper', 'SKILL.md'), 'utf8'), + /^---\nname: graphkeeper\n/, + ); + } finally { + await fixture.cleanup(); + } +}); + +test('explicit Gemini CLI integration creates the managed GEMINI.md block and skill through the CLI', async () => { + const fixture = await createRepositoryFixture(); + try { + const result = await runInit(fixture.root, ['--integrate', 'geminicli', '--yes']); + assert.equal(result.exitCode, EXIT_SUCCESS, result.stderr); + assert.match(result.stdout, /CREATE GEMINI\.md/); + const gemini = await readFile(join(fixture.root, 'GEMINI.md'), 'utf8'); + assert.match(gemini, //); + assert.match(gemini, /invoke `@graphkeeper`/); + assert.equal((gemini.match(/graphkeeper:geminicli:start/g) ?? []).length, 1); + assert.match( + await readFile(join(fixture.root, '.gemini', 'skills', 'graphkeeper', 'SKILL.md'), 'utf8'), + /^---\nname: graphkeeper\n/, + ); + } finally { + await fixture.cleanup(); + } +}); + test('non-interactive integration requires --yes and refuses before mutation', async () => { const fixture = await createRepositoryFixture(); try { @@ -151,8 +214,14 @@ test('--dry-run preflights all adapters without prompting or writing', async () assert.match(result.stdout, /CREATE AGENTS\.md/); assert.match(result.stdout, /CREATE CLAUDE\.md/); assert.match(result.stdout, /CREATE \.cursor\/rules\/graphkeeper\.md/); + assert.match(result.stdout, /CREATE \.kilo\/rules\/graphkeeper\.md/); + assert.match(result.stdout, /CREATE \.windsurf\/rules\/graphkeeper\.md/); + assert.match(result.stdout, /CREATE GEMINI\.md/); assert.match(result.stdout, /\.claude\/skills\/graphkeeper\/SKILL\.md/); assert.match(result.stdout, /\.opencode\/skills\/graphkeeper\/SKILL\.md/); + assert.match(result.stdout, /\.kilo\/skills\/graphkeeper\/SKILL\.md/); + assert.match(result.stdout, /\.windsurf\/skills\/graphkeeper\/SKILL\.md/); + assert.match(result.stdout, /\.gemini\/skills\/graphkeeper\/SKILL\.md/); assert.match(result.stdout, /DRY RUN No changes were made/); assert.doesNotMatch(result.stdout, /Restart Claude Code/); await assert.rejects(stat(join(fixture.root, 'graph'))); @@ -193,6 +262,15 @@ test('all adapters install and conservative removal works through the CLI', asyn await readFile(join(fixture.root, '.cursor', 'rules', 'graphkeeper.md'), 'utf8'), /graphkeeper:cursor/, ); + assert.match( + await readFile(join(fixture.root, '.kilo', 'rules', 'graphkeeper.md'), 'utf8'), + /graphkeeper:kilo/, + ); + assert.match( + await readFile(join(fixture.root, '.windsurf', 'rules', 'graphkeeper.md'), 'utf8'), + /graphkeeper:windsurf/, + ); + assert.match(await readFile(join(fixture.root, 'GEMINI.md'), 'utf8'), /graphkeeper:geminicli/); const refused = await runCli(fixture.root, ['integrate', 'remove', 'claude']); assert.equal(refused.exitCode, EXIT_USAGE); diff --git a/tests/integration/agent-integrations.test.ts b/tests/integration/agent-integrations.test.ts index 89c4617..12dcf69 100644 --- a/tests/integration/agent-integrations.test.ts +++ b/tests/integration/agent-integrations.test.ts @@ -145,6 +145,119 @@ test('OpenCode shares AGENTS.md with Codex and removal preserves the sibling blo } }); +test('Kilo integration installs the canonical skill and one independent guidance block', async () => { + const fixture = await createRepositoryFixture(); + try { + const report = await initialize({ + cwd: fixture.root, + force: false, + integrations: ['kilo'], + environment: supportedInitEnvironment(), + }); + assert.equal( + await readFile(join(fixture.root, '.kilo', 'skills', 'graphkeeper', 'SKILL.md'), 'utf8'), + await template(), + ); + const rules = await readFile( + join(fixture.root, '.kilo', 'rules', 'graphkeeper.md'), + 'utf8', + ); + assert.match(rules, //); + assert.match(rules, /invoke `@graphkeeper`/); + assert.equal((rules.match(/graphkeeper:kilo:start/g) ?? []).length, 1); + assert.ok(report.notes.some((note) => /Restart Kilo Code/.test(note))); + } finally { + await fixture.cleanup(); + } +}); + +test('Windsurf integration installs the canonical skill and one independent guidance block', async () => { + const fixture = await createRepositoryFixture(); + try { + const report = await initialize({ + cwd: fixture.root, + force: false, + integrations: ['windsurf'], + environment: supportedInitEnvironment(), + }); + assert.equal( + await readFile(join(fixture.root, '.windsurf', 'skills', 'graphkeeper', 'SKILL.md'), 'utf8'), + await template(), + ); + const rules = await readFile( + join(fixture.root, '.windsurf', 'rules', 'graphkeeper.md'), + 'utf8', + ); + assert.match(rules, //); + assert.match(rules, /invoke `@graphkeeper`/); + assert.equal((rules.match(/graphkeeper:windsurf:start/g) ?? []).length, 1); + assert.ok(report.notes.some((note) => /Restart Windsurf/.test(note))); + } finally { + await fixture.cleanup(); + } +}); + +test('Gemini CLI integration installs the canonical skill and one independent GEMINI.md block', async () => { + const fixture = await createRepositoryFixture(); + try { + const report = await initialize({ + cwd: fixture.root, + force: false, + integrations: ['geminicli'], + environment: supportedInitEnvironment(), + }); + assert.equal( + await readFile(join(fixture.root, '.gemini', 'skills', 'graphkeeper', 'SKILL.md'), 'utf8'), + await template(), + ); + const gemini = await readFile(join(fixture.root, 'GEMINI.md'), 'utf8'); + assert.match(gemini, //); + assert.match(gemini, /invoke `@graphkeeper`/); + assert.equal((gemini.match(/graphkeeper:geminicli:start/g) ?? []).length, 1); + assert.ok(report.notes.some((note) => /Restart Gemini CLI/.test(note))); + } finally { + await fixture.cleanup(); + } +}); + +test('Kilo, Windsurf, and Gemini CLI removal deletes only canonical owned material and leaves others intact', async () => { + const fixture = await createRepositoryFixture(); + try { + await initialize({ + cwd: fixture.root, + force: false, + integrations: ['kilo', 'windsurf', 'geminicli', 'codex'], + environment: supportedInitEnvironment(), + }); + for (const adapter of ['kilo', 'windsurf', 'geminicli'] as const) { + const plan = await prepareAgentRemoval(fixture.root, adapter); + await applyAgentIntegrationPlan(plan); + } + assert.doesNotMatch( + await readFile(join(fixture.root, '.kilo', 'rules', 'graphkeeper.md'), 'utf8'), + /graphkeeper:kilo/, + ); + assert.doesNotMatch( + await readFile(join(fixture.root, '.windsurf', 'rules', 'graphkeeper.md'), 'utf8'), + /graphkeeper:windsurf/, + ); + assert.doesNotMatch(await readFile(join(fixture.root, 'GEMINI.md'), 'utf8'), /graphkeeper:geminicli/); + await assert.rejects(stat(join(fixture.root, '.kilo', 'skills', 'graphkeeper'))); + await assert.rejects(stat(join(fixture.root, '.windsurf', 'skills', 'graphkeeper'))); + await assert.rejects(stat(join(fixture.root, '.gemini', 'skills', 'graphkeeper'))); + assert.match(await readFile(join(fixture.root, 'AGENTS.md'), 'utf8'), /graphkeeper:codex:start/); + assert.equal( + await readFile(join(fixture.root, '.agents', 'skills', 'graphkeeper', 'SKILL.md'), 'utf8'), + await template(), + ); + + const repeated = await prepareAgentRemoval(fixture.root, 'geminicli'); + assert.ok(repeated.actions.every((action) => action.kind === 'skip')); + } finally { + await fixture.cleanup(); + } +}); + test('multi-adapter installation is deterministic, idempotent, and isolated', async () => { const fixture = await createRepositoryFixture(); try { diff --git a/tests/unit/agent-adapters.test.ts b/tests/unit/agent-adapters.test.ts index f7f3137..184ed09 100644 --- a/tests/unit/agent-adapters.test.ts +++ b/tests/unit/agent-adapters.test.ts @@ -24,7 +24,7 @@ function sourceFile(relativePath: string): Promise { test('registers explicit adapters with independent destinations', () => { assert.deepEqual( AGENT_ADAPTERS.map((adapter) => adapter.id), - ['codex', 'claude', 'cursor', 'opencode'], + ['codex', 'claude', 'cursor', 'opencode', 'kilo', 'windsurf', 'geminicli'], ); assert.deepEqual( AGENT_ADAPTERS.map((adapter) => adapter.skillTarget), @@ -33,11 +33,22 @@ test('registers explicit adapters with independent destinations', () => { '.claude/skills/graphkeeper/SKILL.md', '.cursor/skills/graphkeeper/SKILL.md', '.opencode/skills/graphkeeper/SKILL.md', + '.kilo/skills/graphkeeper/SKILL.md', + '.windsurf/skills/graphkeeper/SKILL.md', + '.gemini/skills/graphkeeper/SKILL.md', ], ); assert.deepEqual( AGENT_ADAPTERS.map((adapter) => adapter.guidanceTarget), - ['AGENTS.md', 'CLAUDE.md', '.cursor/rules/graphkeeper.md', 'AGENTS.md'], + [ + 'AGENTS.md', + 'CLAUDE.md', + '.cursor/rules/graphkeeper.md', + 'AGENTS.md', + '.kilo/rules/graphkeeper.md', + '.windsurf/rules/graphkeeper.md', + 'GEMINI.md', + ], ); assert.notEqual(AGENT_ADAPTERS[0]?.startMarker, AGENT_ADAPTERS[1]?.startMarker); assert.notEqual(AGENT_ADAPTERS[1]?.startMarker, AGENT_ADAPTERS[2]?.startMarker); @@ -68,6 +79,54 @@ test('plans Claude create, append, refresh, and skip without changing outside by assert.equal(planGuidanceContent(adapter, refreshed.content).kind, 'skip'); }); +test('plans Kilo guidance create, append, refresh, and skip without changing outside bytes', () => { + const adapter = getAgentAdapter('kilo'); + const created = planGuidanceContent(adapter, null); + assert.equal(created.kind, 'create'); + assert.match(created.content, /invoke `@graphkeeper`/); + assert.match(created.content, /graphkeeper:kilo:start/); + + const existing = '# Kilo rules\n'; + const appended = planGuidanceContent(adapter, existing); + assert.equal(appended.kind, 'append'); + assert.ok(appended.content.startsWith(existing)); + assert.match(appended.content, /graphkeeper:kilo:start/); + + assert.equal(planGuidanceContent(adapter, created.content).kind, 'skip'); +}); + +test('plans Windsurf guidance create, append, refresh, and skip without changing outside bytes', () => { + const adapter = getAgentAdapter('windsurf'); + const created = planGuidanceContent(adapter, null); + assert.equal(created.kind, 'create'); + assert.match(created.content, /invoke `@graphkeeper`/); + assert.match(created.content, /graphkeeper:windsurf:start/); + + const existing = '# Windsurf rules\n'; + const appended = planGuidanceContent(adapter, existing); + assert.equal(appended.kind, 'append'); + assert.ok(appended.content.startsWith(existing)); + assert.match(appended.content, /graphkeeper:windsurf:start/); + + assert.equal(planGuidanceContent(adapter, created.content).kind, 'skip'); +}); + +test('plans Gemini CLI guidance create into GEMINI.md with its own marked block', () => { + const adapter = getAgentAdapter('geminicli'); + const created = planGuidanceContent(adapter, null); + assert.equal(created.kind, 'create'); + assert.match(created.content, /invoke `@graphkeeper`/); + assert.match(created.content, /graphkeeper:geminicli:start/); + + const existing = '# Gemini context\n'; + const appended = planGuidanceContent(adapter, existing); + assert.equal(appended.kind, 'append'); + assert.ok(appended.content.startsWith(existing)); + assert.match(appended.content, /graphkeeper:geminicli:start/); + + assert.equal(planGuidanceContent(adapter, created.content).kind, 'skip'); +}); + test('rejects missing, repeated, reversed, mixed, and malformed adapter markers', () => { const adapter = getAgentAdapter('claude'); for (const malformed of [ From c39c3dfe338e3c3eb1e5107d85514a299da0f6a6 Mon Sep 17 00:00:00 2001 From: anusbutt Date: Fri, 21 Aug 2026 00:11:41 +0500 Subject: [PATCH 2/2] docs: rewrite README with product-first structure --- README.md | 414 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 268 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index dbc1a18..366ae6b 100644 --- a/README.md +++ b/README.md @@ -4,105 +4,135 @@ [![npm](https://img.shields.io/npm/v/graphkeeper.svg)](https://www.npmjs.com/package/graphkeeper) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -**Coding agents shouldn't just remember. They should be able to prove why they -remember.** +## Persistent, grounded memory for coding agents -GraphKeeper gives coding agents grounded, auditable memory—stored beside the code, -reviewable in Git, and traceable to exact evidence. +Coding agents forget project knowledge between sessions. GraphKeeper gives them +durable memory that is: -GraphKeeper is not a transcript store or a generic memory wrapper. It records durable -project findings as flat claims linked to the run and evidence that produced them. -When a finding becomes outdated, a new claim explicitly supersedes it; the old claim -stays in Git history for review. The result is memory that Codex, Claude Code, and -humans can inspect without trusting an opaque summary or hosted service. +- **grounded in evidence** — every fact points at the exact lines that support it +- **stored in Git** — reviewable like any other change, not hidden in a hosted service +- **traceable to the run** that produced it +- **validated before becoming durable** — a commit hook and `graphkeeper check` reject + bad or tampered records +- **able to supersede outdated knowledge** — corrections append a new claim instead of + rewriting history -The core flow is deliberately small: +GraphKeeper is **Git-native, evidence-backed memory for coding agents**. It is not a +transcript store, a vector database, or a hosted memory service. It does not require a +backend, a database, or a separate memory server. Node.js 18+, npm, and Git are all +you need. -1. An agent discovers a stable project fact and captures the relevant output under - `evidence/`. -2. It appends an entity, run, and evidence-backed claim to `graph/`. +## Why GraphKeeper + +Without durable memory, a coding agent that investigated something in one session +cannot tell a later session what it found, whether that finding is still current, or +where it came from. + +```text +Without GraphKeeper + +Agent session 1: "The payments tests are flaky because of timezone issues." +Agent session 2: "I don't know why the payments tests are flaky." +``` + +With GraphKeeper, the agent records the finding as a claim, captures the evidence it +came from, links both to the run that produced them, and commits the whole thing to +Git. A later session retrieves the claim, inspects its provenance, and continues from +there. + +```text +With GraphKeeper + +Claim ──► Evidence ──► Agent run ──► Git history ──► Next session +``` + +## Core idea + +```text +Agent discovers something + ↓ +Evidence is captured + ↓ +Grounded claim is created + ↓ +GraphKeeper validates it + ↓ +Knowledge is stored in Git + ↓ +Future agent retrieves it + ↓ +Knowledge can be verified or superseded +``` + +The memory contract is deliberately small and explicit. An agent: + +1. captures relevant output under `evidence/`; +2. appends an entity, a run, and an evidence-backed claim to `graph/`; 3. `graphkeeper check` validates schema, provenance, and append-only history against - the committed Git state. -4. A future session retrieves the active claim with `graphkeeper query `. -5. It starts from a directly relevant grounded claim and performs only the freshness - verification the current task requires, while treating the claim as evidence to - assess rather than automatic truth. -6. If the fact changes, the agent appends a new claim with `supersedes`; both + the committed Git state; +4. a future session retrieves the active claim with `graphkeeper query `; +5. it starts from a directly relevant grounded claim and performs only the freshness + verification the current task requires — treating the claim as evidence to assess, + not automatic truth; +6. if the fact changes, the agent appends a new claim with `supersedes`; both generations remain auditable. GraphKeeper does not ingest conversations or decide what should become memory. The -shipped Codex and Claude Code skills give agents the same explicit writing contract. +shipped agent skills give every supported agent the same explicit writing contract. -![How GraphKeeper gives coding agents durable project memory](https://raw.githubusercontent.com/anusbutt/Graph_Keeper/main/docs/assets/graphkeeper-overview.png) +## Works with your coding agent -## Prerequisites +GraphKeeper ships first-class integrations for the agents below. If you use one of +them, GraphKeeper works with it. -- Node.js 18 or newer and npm -- Git +```text +Codex · Claude Code · Cursor · OpenCode · Kilo Code · Windsurf · Gemini CLI +``` -Linux, macOS, Windows through Git Bash or WSL, and native Windows PowerShell are -supported. Native PowerShell does not require a POSIX shell or jq for current -GraphKeeper repositories. +Each integration installs a canonical skill your agent invokes, plus a small reminder +in the agent's guidance file. All skills are generated from one vendor-neutral +`templates/SKILL.md`. -## Installation +| Coding agent | Integration flag | Skill | Guidance | Invocation | +|---|---|---|---|---| +| Codex | `--integrate codex` | `.agents/skills/graphkeeper/SKILL.md` | `AGENTS.md` | `$graphkeeper` | +| Claude Code | `--integrate claude` | `.claude/skills/graphkeeper/SKILL.md` | `CLAUDE.md` | `/graphkeeper` | +| Cursor | `--integrate cursor` | `.cursor/skills/graphkeeper/SKILL.md` | `.cursor/rules/graphkeeper.md` | `@graphkeeper` | +| OpenCode | `--integrate opencode` | `.opencode/skills/graphkeeper/SKILL.md` | `AGENTS.md` | `graphkeeper` | +| Kilo Code | `--integrate kilo` | `.kilo/skills/graphkeeper/SKILL.md` | `.kilo/rules/graphkeeper.md` | `@graphkeeper` | +| Windsurf | `--integrate windsurf` | `.windsurf/skills/graphkeeper/SKILL.md` | `.windsurf/rules/graphkeeper.md` | `@graphkeeper` | +| Gemini CLI | `--integrate geminicli` | `.gemini/skills/graphkeeper/SKILL.md` | `GEMINI.md` | `@graphkeeper` | -Run the current stable release without a permanent installation: +Install any subset, or all of them at once: ```sh -npx graphkeeper@latest --help +npx graphkeeper@latest init --integrate codex --integrate claude +# Or install every registered adapter: +npx graphkeeper@latest init --integrate all ``` -For repeated use, install the CLI globally: +Adapters that share a guidance file (Codex and OpenCode both use `AGENTS.md`) coexist: +each owns one marked block, and your existing guidance outside that block is preserved. -```sh -npm install --global graphkeeper -graphkeeper --help -``` +## Quickstart -The package is published as [`graphkeeper`](https://www.npmjs.com/package/graphkeeper). -GraphKeeper has no runtime npm dependencies. Node.js, npm, and Git are the normal -system prerequisites. - -## Two-minute quickstart +Prerequisites: **Node.js 18+**, **npm**, and **Git**. Supported on Linux, macOS, +Windows via Git Bash or WSL, and native Windows PowerShell. Run this at the root of the repository whose memory you want to protect: ```sh npx graphkeeper@latest init --integrate codex -# Or: npx graphkeeper@latest init --integrate claude -# Or: npx graphkeeper@latest init --integrate all npx graphkeeper@latest check ``` Review the displayed plan and confirm it. `init` creates the JSON graph, `evidence/`, -the canonical validator, a pre-commit hook, and the repository-scoped Codex skill. -`--integrate codex` adds the Codex reminder to `AGENTS.md`; `--integrate claude` adds -the Claude skill and reminder; `--integrate cursor` adds the Cursor skill and rule; -`--integrate opencode` adds the OpenCode skill and reminder; `--integrate kilo` adds -the Kilo Code skill and rule; `--integrate windsurf` adds the Windsurf skill and rule; -`--integrate geminicli` adds the Gemini CLI skill and reminder; -`--integrate all` installs every registered adapter. Codex uses -`.agents/skills/graphkeeper/SKILL.md`, `AGENTS.md`, and -`$graphkeeper`. Claude Code uses `.claude/skills/graphkeeper/SKILL.md`, -`CLAUDE.md`, and `/graphkeeper`. Cursor uses `.cursor/skills/graphkeeper/SKILL.md`, -`.cursor/rules/graphkeeper.md`, and `@graphkeeper`. OpenCode uses -`.opencode/skills/graphkeeper/SKILL.md`, `AGENTS.md`, and `graphkeeper`. Kilo Code uses -`.kilo/skills/graphkeeper/SKILL.md`, `.kilo/rules/graphkeeper.md`, and `@graphkeeper`. -Windsurf uses `.windsurf/skills/graphkeeper/SKILL.md`, `.windsurf/rules/graphkeeper.md`, -and `@graphkeeper`. Gemini CLI uses `.gemini/skills/graphkeeper/SKILL.md`, `GEMINI.md`, -and `@graphkeeper`. All skills -are generated from the same `templates/SKILL.md`. The set of adapters is a closed -data-driven registry in `src/lib/agent-adapters.ts`; `graphkeeper --help` lists the -installed adapters. Adapters that share a guidance file (Codex and OpenCode both use -`AGENTS.md`) coexist, each owning one marked block. Existing guidance outside the -matching marked block is preserved. Integration plans are shown before writing; answer the prompt, or pass -`--yes` in non-interactive automation. Use `--dry-run` for a complete read-only -preflight. Default init and `--force` do not create or change `AGENTS.md` or -`CLAUDE.md`. - -Ask the selected agent to record a verified finding with `$graphkeeper` in Codex or -`/graphkeeper` in Claude Code. After it writes the claim and evidence, validate and -retrieve the result: +the canonical validator, a pre-commit hook, and the repository-scoped skill. +`--integrate codex` also adds the Codex reminder to `AGENTS.md`. + +Now ask your agent to record a verified finding (with `$graphkeeper` in Codex, +`/graphkeeper` in Claude Code, or your agent's invocation from the table above). After +it writes the claim and evidence, validate and retrieve the result: ```sh npx graphkeeper check @@ -110,10 +140,10 @@ npx graphkeeper query test_payments_flaky npx graphkeeper doctor ``` -Commit the generated graph, validator, agent skills, and guidance files. The hook -normally lives under `.git` and is not committed; -`.githooks/pre-commit` is created only when GraphKeeper must preserve and chain another -hook. An empty `evidence/` directory becomes tracked with the first captured artifact. +Commit the generated graph, validator, agent skill, and guidance file. The hook +normally lives under `.git` and is not committed; `.githooks/pre-commit` is created +only when GraphKeeper must preserve and chain another hook. An empty `evidence/` +directory becomes tracked with the first captured artifact. To try the full discovery-to-supersession flow with populated data, copy `examples/worked-example/graph` and `examples/worked-example/evidence` into a temporary @@ -122,107 +152,199 @@ Git repository, then query `test_payments_flaky`. ## Before and after Without grounded memory, a later session may only remember: “the payments test was -flaky.” It cannot tell whether that conclusion is current or where it came from. The -worked example records the initial failure as `claim_11111111`, captures a passing UTC -rerun, and appends `claim_22222222` with `supersedes: "claim_11111111"`. +flaky.” It cannot tell whether that conclusion is current or where it came from. + +The [worked example](examples/worked-example/README.md) records the initial failure as +`claim_11111111`, captures a passing UTC rerun, and appends `claim_22222222` with +`supersedes: "claim_11111111"`. With GraphKeeper, the active correction remains a flat, reviewable claim: ```text Entity: test_payments_flaky +Active claims: 2 + Claim: claim_22222222 - Predicate: has_root_cause - Object: "timezone_default" + Predicate: has_status + Object: "passing_with_utc_default" Source: tool_output + Command: "TZ=UTC npm test -- payments" + Exit code: 0 Evidence: evidence/utc-rerun.log#L1-L3 - Producer: run_2026-07-21-utc_fix + Producer: run_2026-07-21-utc_rerun ``` -The older claim remains in history and is marked as superseded. Reviewers can follow the claim ID to the run and exact evidence lines instead of trusting an ungrounded summary. +The older claim remains in history and is marked as superseded. Reviewers can follow +the claim ID to the run and exact evidence lines instead of trusting an ungrounded +summary. + +## What gets stored + +GraphKeeper puts a small, human-readable structure beside your code: + +```text +graph/ + entities.json canonical subjects (stable IDs, aliases, source documents) + claims.json flat, evidence-backed facts about those subjects + runs.json the agent runs that produced claims and captured evidence + SCHEMA.md the generated record contract your agents follow +evidence/ append-only captured artifacts (logs, output, tool results) +scripts/ + validate.mjs the canonical fast validator (used by check and the hook) + validate.sh legacy compatibility fallback (for unmigrated repos) +.agents/skills/graphkeeper/SKILL.md + the Codex agent skill (plus per-agent skills after --integrate) +``` + +Entities are the subjects of memory, claims are the grounded facts, runs record how a +claim came to be, and evidence is the captured output a claim points to. Everything is +plain JSON and text — no database, no server, no vector index. + +## How it works + +```text + CODING AGENTS + ┌────────┬────────┬────────┬────────┐ + │ Codex │ Claude │ Cursor │ OpenCode│ ... Kilo · Windsurf · Gemini + └────────┴───┬────┴────────┴────────┘ + │ skill + guidance + ▼ + GraphKeeper CLI + ┌────────────┼────────────┐ + ▼ ▼ ▼ + Claims Runs Evidence + └────────────┼────────────┘ + ▼ + Git repository + (validated by hook + check) + ▼ + Future agent sessions +``` + +An agent writes to `graph/` and `evidence/`, and `graphkeeper check` (and the +installed pre-commit hook) validates the result. A future session calls +`graphkeeper query ` to read active claims with their provenance. ## Commands | Command | Role | |---|---| -| `graphkeeper init [--force] [--integrate ]... [--yes] [--dry-run]` | Scaffold safely and optionally install explicit agent adapters (currently `codex`, `claude`, `cursor`, `opencode`, `kilo`, `windsurf`, `geminicli`). Distinct `--integrate` flags may repeat; `all` must stand alone. `--yes --dry-run` is accepted as a harmless dry run. | -| `graphkeeper integrate remove [--yes] [--dry-run]` | Remove only recognizable GraphKeeper-owned material for one adapter. Modified skills and unexpected supporting files are preserved for manual review. | -| `graphkeeper check` | Run the same fast schema, append-only history, and committed-evidence protection checks used by the Git hook. | +| `graphkeeper init [--force] [--integrate ]... [--yes] [--dry-run]` | Scaffold the graph and optionally install agent adapters. Distinct `--integrate` flags may repeat; `all` must stand alone. `--yes --dry-run` is accepted as a harmless dry run. | +| `graphkeeper integrate remove [--yes] [--dry-run]` | Remove only recognizable GraphKeeper-owned material for one adapter. Modified skills and unexpected supporting files are preserved. | +| `graphkeeper check` | Run the same fast schema, append-only history, and committed-evidence checks used by the Git hook. | | `graphkeeper query ` | Resolve an exact ID or unique alias and print active claims with provenance. It does not read evidence contents. | | `graphkeeper doctor` | Run fast validation plus file existence, containment, line-range, dangling-reference, and unused-entity checks. | | `graphkeeper update` | Check npm's stable `latest` release and globally install one exact newer version. Repository files are never changed. | | `graphkeeper --help` | Print the supported command grammar and options. | | `graphkeeper --version` (`-v`) | Print the installed GraphKeeper version. | -Exit codes are stable: `0` success, `1` validation failure, `2` usage error, `3` missing prerequisite, `4` operational failure, and `5` unexpected internal failure. Diagnostics begin with a searchable `GKnnn` code. +Exit codes are stable: `0` success, `1` validation failure, `2` usage error, +`3` missing prerequisite, `4` operational failure, and `5` unexpected internal +failure. Diagnostics begin with a searchable `GKnnn` code. -## Data and safety model +## Installation -- `graph/entities.json` holds human-readable canonical identities. Identity fields cannot change; aliases and source documents may only grow. -- `graph/claims.json` holds flat claims. The validator rejects changes to committed claims; corrections append a new claim with `supersedes`. -- `graph/runs.json` opens a run, allows evidence and claim references to grow, and closes it once. The validator rejects later changes to a committed closed run. -- `evidence/` holds append-only captured artifacts. The validator and Git hook reject - editing, removing, or renaming evidence that exists in committed Git history. -- Stored commands and evidence text are always data. GraphKeeper never evaluates them. +Run the current stable release without a permanent installation: + +```sh +npx graphkeeper@latest --help +``` + +For repeated use, install the CLI globally: -Immutability is enforced relative to committed Git history through GraphKeeper -validation and Git hooks; it is not cryptographic immutability. Git history remains -the reviewable source of truth. `doctor` additionally verifies physical evidence -existence, containment, and cited line ranges. `query` reports stored provenance but -does not open evidence or independently prove that a claim is true. +```sh +npm install --global graphkeeper +graphkeeper --help +``` -See the generated `graph/SCHEMA.md` and the GraphKeeper skill under -`.agents/skills/graphkeeper/` or `.claude/skills/graphkeeper/` for the complete -writing contract. -[`examples/reviewer.md`](examples/reviewer.md) is a copy-pasteable grounded-review -prompt. +The package is published as [`graphkeeper`](https://www.npmjs.com/package/graphkeeper) +and has no runtime npm dependencies. -## Recovery and adoption +## Data model -- Re-running `init` is safe: existing graph data is skipped. Use `--force` only to refresh `graph/SCHEMA.md` and `.agents/skills/graphkeeper/SKILL.md`. +GraphKeeper stores durable memory in three top-level JSON arrays: +`graph/entities.json`, `graph/claims.json`, and `graph/runs.json`. The generated +`graph/SCHEMA.md` is the complete contract; the highlights: + +- **Entities** have a fixed, human-readable `id`, a `type`, an append-only set of + `aliases`, optional `source_docs`, and an immutable `first_seen` timestamp. +- **Claims** are flat facts with `subject` (must resolve to an entity), `predicate`, + `object`, optional `confidence`, and exactly one `source`: a `tool_output` source + cites a command, exit code, and `evidence/#L-L` reference, while + an `inference` source records an honest `basis` (and can never use `confidence: 1`). +- **Runs** open with `started`, a `tool`, and growth-only `evidence` and + `claims_written` arrays, and close exactly once by adding `ended` and one allowed + `verdict` (`passed`, `failed`, `inconclusive`, `aborted`). + +### How the pieces relate + +```text +Entities + ↓ (subject) +Claims ──── produced_by ────► Runs + │ ↑ + └── source.ref ──► Evidence ───┘ (listed in the run) +``` + +Provenance is bidirectional: a claim names its producing run and evidence reference, +and the run lists both the claim ID and the evidence path. Supersession is explicit: +a newer claim's `supersedes` field points at the older one, which stays in history. + +Despite the name, GraphKeeper is **not a graph database**. It uses flat, Git-reviewable +JSON records. The "graph" is the conceptual network of entities, claims, evidence, and +runs, not an indexed graph store. + +### Worked example + +The complete flow, including a failing run, a passing UTC rerun that supersedes the +first claim, and an honest inference, lives in +[`examples/worked-example`](examples/worked-example/README.md). Run its populated graph +in a scratch repository and observe how `query` reports the active correction while +the superseded claim stays in history. `examples/reviewer.md` is a copy-pasteable +grounded-review prompt. + +## Validation, recovery, and edge cases + +- **Immutability** is enforced relative to committed Git history through GraphKeeper + validation and Git hooks; it is not cryptographic immutability. Git history remains + the reviewable source of truth. +- Claims and committed evidence are append-only. Corrections append a successor with + `supersedes`; they do not rewrite old data. Recover an accidental staged edit with + your normal Git workflow; do not “fix” committed history by rewriting IDs. +- `doctor` verifies physical evidence existence, containment, and cited line ranges. + `query` reports stored provenance but does not open evidence or independently prove + that a claim is true. +- Re-running `init` is safe: existing graph data is skipped. Use `--force` only to + refresh `graph/SCHEMA.md` and the generated skill. - For a `GKnnn` failure, use the [diagnostic reference](docs/diagnostics.md) to identify the emitting command, exit class, and safe recovery before changing graph data. -- A root `SKILL.md` created by an older GraphKeeper version is legacy user content. It is reported and preserved; migrate by committing the generated `.agents/skills/graphkeeper/SKILL.md`. -- `--integrate codex` manages the Codex skill plus one marked block in `AGENTS.md`; - `--integrate claude` does the same for the Claude skill and `CLAUDE.md`. Multiple - distinct flags and `--integrate all` use one plan and one confirmation. -- Integration creates a guidance file when absent, appends one block when no markers - exist, and refreshes only that block later. Malformed, mixed, repeated, reversed, - wrong-type, symlinked, or concurrently changed destinations fail with `GK004`. -- Non-interactive integration and removal require `--yes`; declined prompts and EOF - leave the repository unchanged. `--dry-run` never prompts or writes. -- `graphkeeper integrate remove ` removes an exact canonical skill and its - matching block. User-modified skills and directories with unexpected files are - preserved with manual-cleanup instructions. -- Restart Claude Code once if the current session began before the repository's - top-level `.claude/skills/` directory was created. -- Run `graphkeeper update` from any supported shell, including native PowerShell, to - update a global npm installation. It resolves the stable published version, installs only when that version is - newer, and does not install prereleases. If the registry is offline, no update is - attempted; retry when npm registry access returns. -- After updating an existing installation to 0.4.0, rerun `graphkeeper init --force` - to refresh the repository skill and schema. This preserves `scripts/validate.sh`. - Exact package-owned validators and hooks migrate to the Node path automatically. - If a repository has a customized shell-only - validator, review and migrate it manually; that legacy fallback can still require - a POSIX `sh` and jq until migration is complete. Commit refreshed guidance and - validators together so every contributor uses the same contract. - See the [native Windows migration guide](docs/windows-migration.md) for package-owned - and customized repository paths. -- A global npm permission error returns `GK004` without changing the repository. - Configure npm through a Node version manager or a user-writable npm prefix, then - retry. See npm's - [global installation guidance](https://docs.npmjs.com/downloading-and-installing-packages-globally/). -- In a non-Git directory, files are scaffolded but hook enforcement is disabled until `git init` and another `graphkeeper init`. -- If `.git/hooks/pre-commit` already belongs to another tool, GraphKeeper does not overwrite it. It writes `.githooks/pre-commit` and prints chaining instructions. -- If `core.hooksPath` is set, GraphKeeper installs there. Resolve any existing non-GraphKeeper hook explicitly rather than deleting it. -- When a commit is blocked, run `graphkeeper check`, fix every reported `GKnnn` violation, and stage the corrected files again. Run `graphkeeper doctor` for missing files or bad line ranges. -- Graph records are append-only by semantics. Recover an accidental staged edit with your normal Git workflow; do not “fix” committed history by rewriting IDs. - -## V1 limits and future path - -V1 is designed for one graph in one repository, up to about 10,000 claims, 2,000 entities, and 1,000 runs on a local SSD. Release gates target p95 under 3 seconds for `check`, under 2 seconds for `query`, under 10 seconds for `doctor`, and under 256 MB peak memory. It has no server, database, authentication, dashboard, telemetry, vector search, or multi-repository synchronization. - -When linear JSON scans or concurrent-write collisions become material, a future storage adapter may preserve the same IDs, provenance, supersession, and run-lifecycle contracts on SQLite or PostgreSQL. That migration is documentation-only in v1; the JSON files remain the source of truth. +- Integration creates a guidance file when absent, appends one marked block when no + markers exist, and refreshes only that block later. Malformed, mixed, repeated, + reversed, wrong-type, symlinked, or concurrently changed destinations fail with + `GK004`. Non-interactive integration and removal require `--yes`; `--dry-run` never + prompts or writes. +- If `.git/hooks/pre-commit` already belongs to another tool, GraphKeeper does not + overwrite it. It writes `.githooks/pre-commit` and prints chaining instructions. +- When a commit is blocked, run `graphkeeper check`, fix every reported `GKnnn` + violation, and stage the corrected files again. Run `graphkeeper doctor` for missing + files or bad line ranges. +- Stored commands and evidence text are always data. GraphKeeper never evaluates them. +- After updating an existing installation to 0.4.0+, rerun `graphkeeper init --force` + to refresh the repository skill and schema. See the + [native Windows migration guide](docs/windows-migration.md). + +## Limitations + +V1 is designed for one graph in one repository, up to about 10,000 claims, 2,000 +entities, and 1,000 runs on a local SSD. Release gates target p95 under 3 seconds for +`check`, under 2 seconds for `query`, under 10 seconds for `doctor`, and under 256 MB +peak memory. It has no server, database, authentication, dashboard, telemetry, vector +search, or multi-repository synchronization. + +When linear JSON scans or concurrent-write collisions become material, a future +storage adapter may preserve the same IDs, provenance, supersession, and run-lifecycle +contracts on SQLite or PostgreSQL. That migration is documentation-only in v1; the JSON +files remain the source of truth. ## Contributing and release status