The source of truth is the commit history itself — the
Ai-Codekeep-Ledger trailers (and git-ai/whogitit notes, where present)
that travel with every commit. Commits merge, rebase, push, and clone
because git already knows how to do that; nothing ai-codekeep stores can
get out of sync with them, because everything ai-codekeep stores is
derived from them.
Three files live under .ai-codekeep/ at the repo root, split by role:
config.json— committed. The team's shared settings (mode, ask-for-message). The only file meant to be pushed.ledger.jsonandstate.json— local caches, never committed. Deterministically rebuilt from commit history: same history in, same ledger out, so two clones can't disagree and there is nothing to merge-conflict. ai-codekeep writes a.ai-codekeep/.gitignorecovering both the first time it runs, so this is enforced automatically. If a cache ever seems wrong (heavy rebases, stats accumulated from abandoned branches),ai-codekeep update --rebuildthrows it away and re-derives from the current branch's history.
See SECURITY.md if reason text might ever contain
sensitive detail — reasons live in commit messages, so they go wherever
your commits go.
{
"mode": "standard",
"ask_for_commit_message": false,
"updated": "2026-07-09T12:00:00.000Z"
}ask_for_commit_message (default false) controls whether the agent
drafts the commit subject/body itself, or asks the user for it first and
uses it verbatim before appending the trailer — see
SKILL.md.
{
"last_processed": "369add62ba9fd676e0d21c8317cfe7ce949e69b6",
"processed": [
"1c0ffee4a1b2c3d4e5f60718293a4b5c6d7e8f90",
"369add62ba9fd676e0d21c8317cfe7ce949e69b6"
]
}processed is the full set of commits ever ingested, and it's what makes
update idempotent: re-running it on a commit that's already in the ledger
is a no-op, so overlapping triggers (a post-commit hook and an agent
both running update --commit HEAD) can't double-count anything. It grows
by one 40-char SHA per commit — negligible for years of typical history.
last_processed is only an optimization: it marks how far a bare
update has caught up, so the next one can scan last_processed..HEAD
instead of the whole log. Targeted --commit/--range runs never advance
it — they only looked at the commits they were given. If the commit it
points at is rewritten away (rebase, squash, amend), update warns and
rescans the full history; the processed set keeps that rescan from
double-counting the commits that survived.
One entry per file path, keyed by path relative to repo root.
last_ai_commit_at is a Unix timestamp (seconds) taken from the commit's
author date (git show --format=%at); ai-codekeep report derives
days_since_ai_touch from it at read time rather than storing a
constantly-stale precomputed value:
{
"src/auth/session.ts": {
"ai_share": 0.71,
"last_touched_by": "human",
"last_ai_commit_at": 1751500000,
"confidence": "self-attested",
"total_lines_seen": 184,
"fix_count": 3,
"revert_count": 0,
"incident_count": 1,
"risk_score": 0.64,
"history": [
{ "commit": "a1b2c3d", "reason": "initial implementation", "class": "feature", "author": "ai", "incident": false },
{ "commit": "d4e5f6a", "reason": "fix race condition (issue #88)", "class": "bugfix-of-ai-code", "author": "human", "incident": true }
]
}
}This is the only place ai-codekeep writes into a commit message, and it's the durable record: it merges, pushes, and clones with the commit it belongs to, which is exactly why the derived caches above can be local and disposable. Query the ledger (that's what it's for — nobody should be grepping commit messages for stats), but understand that the trailer is what actually persists and travels; the ledger can always be rebuilt from it. And because the trailer lives in commit messages, it stays deliberately tiny: a growing block of provenance trailer lines is exactly the kind of commit-message noise that disrupts teams with clean-history or squash-merge conventions, so it's capped at two lines, regardless of mode.
Two trailer lines, at most:
Ai-Codekeep-Ledger: ai=82;class=bugfix-of-ai-code;incident=false
Change-Reason: fix null-deref reported in issue #412
Ai-Codekeep-Ledger is a single ;-separated key=value list — never
multiple separate trailer lines, so escalating detail across modes (see
MODES.md) only ever adds fields to this one value, it doesn't
add new lines:
| Field | Meaning | Present from |
|---|---|---|
ai |
AI-authored percentage for this commit (0-100) | minimal |
class |
feature/refactor/bugfix/bugfix-of-ai-code/revert |
standard |
incident |
true only if genuinely tied to a production incident, otherwise omitted/false |
standard |
model |
model identifier, e.g. claude-sonnet-5 |
audit |
session |
opaque session/prompt reference for full traceability | audit |
Change-Reason is plain free text, not part of the compact trailer — a
reason is meant to be read as prose, not parsed, so it stays as ordinary
human-readable content rather than being packed into key=value form.
bin/ledger.mjs checks refs/notes/ai and refs/notes/whogitit for each
commit before falling back to trailers. Neither tool's schema is assumed
fixed — notesFileShare() in bin/ledger.mjs looks for a handful of
plausible field names (files, attribution, lines, each with an
aiShare/ai_share field) and falls back to the trailer-based estimate if
none match. If you're on a fork of either tool with a different notes
schema, that's the one function to adjust.