Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ transcript store.
intentionally stay out of the fast hook path.
- `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 Codex and Claude Code adapter
registry. Preserve independent destinations, marked-block ownership, planning,
rollback, and conservative removal.
- `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;
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
malformed markers stay rejected with `GK004`. Preserve independent destinations,
marked-block ownership, planning, rollback, and conservative removal.
- `examples/`, `tests/`, `docs/`, and `.github/` contain demonstrations, coverage,
supporting documentation, and repository automation respectively.

Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ versioning; while the package is below 1.0, minor releases may change public beh

## [Unreleased]

### Added

- A Cursor adapter registered as `--integrate cursor` with the canonical skill at
`.cursor/skills/graphkeeper/SKILL.md`, the marked reminder at
`.cursor/rules/graphkeeper.md`, and the `@graphkeeper` invocation. It participates in
`--integrate all`, `--dry-run`, and conservative `integrate remove`.
- An OpenCode adapter registered as `--integrate opencode` with the canonical skill at
`.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`.

### Changed

- Agent adapters are now a closed, data-driven registry. Adding an adapter is a single
entry in `src/lib/agent-adapters.ts`; the CLI `--integrate` grammar, the
`--integrate all` expansion, and removal machinery all derive from it, and `AgentId`
is derived from the registry instead of a hardcoded union.
- The `init` skill-scaffolding special case is expressed as adapter data
(`scaffoldSkillByInit`) instead of a hardcoded agent id.
- Multiple registered adapters may now share one guidance file. Codex and OpenCode both
use `AGENTS.md`; each owns one marked block, sibling blocks are allowed when properly
paired, and unknown or malformed markers are still rejected with `GK004`. Installing
adapters that share a file in one plan (for example `--integrate all`) appends each
block sequentially.

## [0.4.1] - 2026-08-14

### Added
Expand Down
30 changes: 30 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,33 @@
content.
- Restart Claude Code if the current session began before the repository's
`.claude/skills/` directory was created.

## Development workflow: follow the SDD cycle

Every feature or behavior change MUST proceed in this order, per the constitution
(`.specify/memory/constitution.md`) and the canonical `AGENTS.md`:

1. **Constitution** — amend only if a principle changes (documented rationale + impact
report + version bump). Most features need no amendment.
2. **Spec** — write `specs/NNN-short-name/spec.md` (goal, prioritized user stories with
acceptance scenarios, requirements, success criteria). Get it approved before planning.
3. **Plan** — write `specs/NNN-short-name/plan.md` (summary, technical context,
constitution check, sequence, files changed, risks).
4. **Tasks** — write `specs/NNN-short-name/tasks.md` (test-first, phase-ordered, grouped
by user story).
5. **Implementation** — write or update tests FIRST, observe them fail for the intended
reason, then implement the smallest change to pass. Do not implement before the
spec, plan, and tasks exist.

### Rules of the cycle

- Do NOT jump straight to implementation. Stop and produce each artifact in order; the
user can approve or redirect at each step.
- Tests are mandatory and precede the code they govern (red → green). Cover both accepted
behavior and its rejection boundary.
- Keep changes small, focused, and test-backed. Preserve stable `GKnnn` diagnostics, exit
codes, schema fields, and append-only semantics.
- `specs/`, `.specify/`, `.codex/`, `history/`, and `PROGRESS.md` are git-ignored local SDD
artifacts. They are handoff material, not commits or package content.
- Run the smallest relevant focused test while developing, then `npm run typecheck` and the
complete suite before handoff.
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,19 @@ 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 all` does both. Codex uses
the Claude skill and reminder; `--integrate cursor` adds the Cursor skill and rule;
`--integrate opencode` adds the OpenCode 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`. Both skills are generated from the same
`templates/SKILL.md`. Existing guidance outside the matching marked block is
preserved. Integration plans are shown before writing; answer the prompt, or pass
`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
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`.
Expand Down Expand Up @@ -131,8 +138,8 @@ The older claim remains in history and is marked as superseded. Reviewers can fo

| Command | Role |
|---|---|
| `graphkeeper init [--force] [--integrate <codex\|claude\|all>]... [--yes] [--dry-run]` | Scaffold safely and optionally install explicit Codex and/or Claude adapters. Distinct `--integrate` flags may repeat; `all` must stand alone. `--yes --dry-run` is accepted as a harmless dry run. |
| `graphkeeper integrate remove <codex\|claude> [--yes] [--dry-run]` | Remove only recognizable GraphKeeper-owned material for one adapter. Modified skills and unexpected supporting files are preserved for manual review. |
| `graphkeeper init [--force] [--integrate <adapter\|all>]... [--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 integrate remove <adapter> [--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 <subject>` | 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. |
Expand Down
37 changes: 35 additions & 2 deletions docs/agent-integrations.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
# Agent integration architecture

GraphKeeper v1 supports two explicit internal adapters:
GraphKeeper v1 supports the following explicit internal adapters:

| ID | Skill | Guidance | Invocation |
|---|---|---|---|
| `codex` | `.agents/skills/graphkeeper/SKILL.md` | `AGENTS.md` | `$graphkeeper` |
| `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` |

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
from other registered adapters are allowed when properly paired, and planning, append,
and remove always touch only the owning adapter's span. Unknown or malformed markers
are still rejected with `GK004`.

The closed registry in `src/lib/agent-adapters.ts` defines these destinations,
invocations, unique markers, and post-install notes. It is an implementation detail,
not a public plugin framework. Both skill files are rendered byte-for-byte from
not a public plugin framework. Every skill file is rendered byte-for-byte from
`templates/SKILL.md`; vendor-specific text belongs only in adapter metadata and the
short marked reminder.

## Adding an adapter

Adapters are pure data. Adding a new agent is a single entry in the `adapters` array
in `src/lib/agent-adapters.ts`; the command logic, the CLI `--integrate` grammar, the
`--integrate all` expansion, and the removal machinery all derive from that registry
automatically.

An entry provides:

- `id` — the CLI token for `--integrate <id>` and `integrate remove <id>`.
- `displayName` — human-readable label used in plans and diagnostics.
- `skillTarget` — where the canonical `SKILL.md` is written.
- `guidanceTarget` — which file receives the marked reminder block.
- `invocation` — how the agent invokes the skill, used in the reminder text.
- `startMarker` / `endMarker` — HTML-comment markers delimiting GraphKeeper's owned block.
- `scaffoldSkillByInit` — set `true` only when the skill is also scaffolded by
`graphkeeper init` (as for Codex), so the integration step skips rewriting it.
- `postInstallNote` — optional restart hint surfaced after installation.

To keep the registry well-formed, every entry must satisfy `AgentAdapter` (enforced by
the typed `AGENT_ADAPTERS` assignment), ids must be unique, and `AgentId` is derived
from the array. Tests under `tests/unit/agent-adapters.test.ts` guard these invariants.
Before choosing destinations, confirm the target product's actual skill/rules layout;
the Cursor paths above are a reasonable default, not a substitute for product docs.

## Command grammar

`init` accepts distinct repeated `--integrate codex` and
Expand Down
9 changes: 5 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ export interface CliTerminal {

const VERSION = '0.4.1';
const COMMANDS = new Set(['init', 'integrate', 'check', 'query', 'doctor', 'update']);
const AGENT_GRAMMAR = AGENT_IDS.join('|');

const USAGE = [
'GraphKeeper - grounded, auditable memory for coding agents',
'',
'Usage:',
' graphkeeper init [--force] [--integrate <codex|claude|all>]... [--yes] [--dry-run]',
' graphkeeper integrate remove <codex|claude> [--yes] [--dry-run]',
` graphkeeper init [--force] [--integrate <${AGENT_GRAMMAR}|all>]... [--yes] [--dry-run]`,
` graphkeeper integrate remove <${AGENT_GRAMMAR}> [--yes] [--dry-run]`,
' graphkeeper check',
' graphkeeper query <subject>',
' graphkeeper doctor',
Expand Down Expand Up @@ -248,7 +249,7 @@ export async function run(
if (parsed === null) {
io.stderr(diagnostic(
'GK002',
'init accepts --force, distinct --integrate codex|claude flags or --integrate all, --yes, and --dry-run',
'init accepts --force, distinct --integrate ' + AGENT_GRAMMAR + ' flags or --integrate all, --yes, and --dry-run',
));
return EXIT_USAGE;
}
Expand Down Expand Up @@ -287,7 +288,7 @@ export async function run(
if (parsed === null) {
io.stderr(diagnostic(
'GK002',
'integrate accepts remove <codex|claude> followed by optional --yes and --dry-run',
'integrate accepts remove <' + AGENT_GRAMMAR + '> followed by optional --yes and --dry-run',
));
return EXIT_USAGE;
}
Expand Down
4 changes: 3 additions & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,9 @@ export async function prepareInitialization(
const integrationPlan = integrations.length === 0
? null
: await prepareAgentInstall(root, integrations, options.force, {
skipSkillFor: new Set<AgentId>(['codex']),
skipSkillFor: new Set<AgentId>(
AGENT_IDS.filter((id) => getAgentAdapter(id).scaffoldSkillByInit === true),
),
});
const assets = await loadRequiredAssets(scaffoldActions);
const hookPlan = isGitRepository ? await prepareHookPlan(root) : null;
Expand Down
37 changes: 25 additions & 12 deletions src/commands/integrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,18 @@ export async function prepareAgentInstall(
const operations: FileOperation[] = [];
const notes: string[] = [];
const fileSnapshots: FileSnapshot[] = [];
const snapshotTargets = new Set<string>();
const pushSnapshot = (relativeTarget: string, content: string | null): void => {
if (snapshotTargets.has(relativeTarget)) return;
snapshotTargets.add(relativeTarget);
fileSnapshots.push({ relativeTarget, content });
};
const plannedGuidance = new Map<string, string | null>();

for (const id of adapterIds) {
const adapter = getAgentAdapter(id);
const existingSkill = await readOptionalRegularFile(root, adapter.skillTarget);
fileSnapshots.push({
relativeTarget: adapter.skillTarget,
content: existingSkill,
});
pushSnapshot(adapter.skillTarget, existingSkill);
if (!options.skipSkillFor?.has(id)) {
const skillKind = existingSkill === null
? 'create'
Expand Down Expand Up @@ -202,11 +206,11 @@ export async function prepareAgentInstall(
}
}

const existingGuidance = await readOptionalRegularFile(root, adapter.guidanceTarget);
fileSnapshots.push({
relativeTarget: adapter.guidanceTarget,
content: existingGuidance,
});
let existingGuidance = plannedGuidance.get(adapter.guidanceTarget);
if (existingGuidance === undefined) {
existingGuidance = await readOptionalRegularFile(root, adapter.guidanceTarget);
}
pushSnapshot(adapter.guidanceTarget, existingGuidance);
const guidance = planGuidanceContent(adapter, existingGuidance);
actions.push({
kind: guidance.kind,
Expand All @@ -223,6 +227,7 @@ export async function prepareAgentInstall(
expected: guidance.expected,
mode: 0o644,
});
plannedGuidance.set(adapter.guidanceTarget, guidance.content);
}

if (adapter.postInstallNote !== undefined) {
Expand Down Expand Up @@ -436,11 +441,19 @@ export async function validateAgentIntegrationPlan(
throw operational(snapshot.relativeTarget + ' changed after planning and was preserved');
}
}
const writeCounts = new Map<string, number>();
for (const operation of plan.operations) {
if (operation.type === 'write') {
const current = await readOptionalRegularFile(plan.root, operation.relativeTarget);
if (current !== operation.expected) {
throw operational(operation.relativeTarget + ' changed after planning and was preserved');
writeCounts.set(operation.relativeTarget, (writeCounts.get(operation.relativeTarget) ?? 0) + 1);
}
}
for (const operation of plan.operations) {
if (operation.type === 'write') {
if ((writeCounts.get(operation.relativeTarget) ?? 0) <= 1) {
const current = await readOptionalRegularFile(plan.root, operation.relativeTarget);
if (current !== operation.expected) {
throw operational(operation.relativeTarget + ' changed after planning and was preserved');
}
}
continue;
}
Expand Down
Loading