CLI that detects when code changes make docs stale, classifies drift severity via AI, and invokes agents to update docs and open merge requests.
Three-layer pipeline:
- Detection. Compare AST fingerprints of code at provenance vs HEAD. Deterministic, no AI. Outputs a drift report.
- Triage. Classify each drifted anchor as
no_update,minor, ormajorvia a lightweight LLM call. - Remediation. Invoke an external agent to update the docs and open an MR.
no_updateanchors get their provenance advanced without doc changes (pass--no-stampin CI to defer this tokedge sync). kedge's pipeline ends when the agent returns. Your review workflows handle MR approval, CI, and merging.
Any markdown file with kedge: frontmatter becomes a tracked doc: standalone files, AGENTS.md, CLAUDE.md, or anything else. kedge calls these "steering files" (a term from Kiro), but the tool is agent-agnostic.
curl -fsSL https://raw.githubusercontent.com/danielhirt/kedge/main/install.sh | shThe script detects your platform and downloads the latest release binary to /usr/local/bin. Override the install directory with KEDGE_INSTALL_DIR:
curl -fsSL https://raw.githubusercontent.com/danielhirt/kedge/main/install.sh | KEDGE_INSTALL_DIR=~/.local/bin shbrew install danielhirt/tap/kedgedocker run --rm -v "$PWD:/repo" -w /repo danielhirt/kedge checkOr build from source:
docker build -t kedge .Download from GitHub Releases. Binaries cover Linux, macOS, and Windows (x86_64 and aarch64). Each archive includes a .sha256 checksum file.
Requires Rust 1.70+ and git on PATH.
cargo install --path .For air-gapped runners, pre-build the binary and add it to your runner image:
COPY kedge /usr/local/bin/kedgeOn developer machines, kedge install --link symlinks steering files. In CI, kedge install --workspace copies them.
# In your code repository:
kedge init # Creates kedge.toml with defaults
# Edit kedge.toml with your repos, agent command, and triage provider
kedge link # Stamp initial provenance on all doc anchors
kedge check # Detect drift (exit 0 = clean, exit 1 = drift)
kedge update # Full pipeline: detect -> triage -> agent -> MRkedge.toml lives in your code repository root.
[detection]
# exclude_dirs = [".git", "node_modules", "target", ".venv", "__pycache__", ".tox", "vendor"]
[triage]
provider = "anthropic" # "anthropic", "openai", "command", or "none"
model = "claude-haiku-4-5-20251001" # required for anthropic/openai providers
# api_url = "" # custom API endpoint (enterprise proxy/gateway)
# api_key_env = "" # env var name for API key (default: ANTHROPIC_API_KEY or OPENAI_API_KEY)
# triage_timeout = 120 # seconds per doc (default: 120)
# triage_env = { } # extra env vars for command provider
# severity_levels = ["no_update", "minor", "major"]
[remediation]
agent_command = "your-agent-command" # receives JSON on stdin, prints result to stdout
auto_merge_severities = ["no_update"]
# batch = true # single agent invocation for all drifted docs
# agent_timeout = 300 # seconds, kills agent process if exceeded (default: 300)
# agent_env = { } # extra env vars passed to agent process
# agent_instructions = "" # replaces default agent instructions when set
[repos]
# git_timeout = 300 # seconds for clone/fetch operations (default: 300)
[[repos.docs]]
url = "[email protected]:platform/docs.git"
path = "steering/"
ref = "main"
# remote_name = "origin" # git remote name for fetch (default: "origin")
[[agents]]
name = "kiro"
global_steering = "~/.kiro/steering/"
workspace_steering = ".kiro/steering/"
agents_file = "AGENTS.md"
skill_dir = ".kiro/skills/"
[[agents]]
name = "claude"
global_steering = "~/.claude/docs/"
workspace_steering = "docs/"
agents_file = "CLAUDE.md"
skill_dir = ".claude/skills/"| Section | Field | Default | Description |
|---|---|---|---|
[detection] |
exclude_dirs |
.git, node_modules, ... |
Directories to skip when scanning for docs |
[triage] |
provider |
"command" |
AI provider: anthropic, openai, command, or none |
model |
Model ID (required for anthropic/openai) |
||
api_url |
provider default | Custom API endpoint for enterprise proxies | |
api_key_env |
ANTHROPIC_API_KEY or OPENAI_API_KEY |
Env var name holding the API key | |
triage_command |
Shell command for command provider |
||
triage_timeout |
120 |
Seconds per triage call | |
triage_env |
{} |
Extra env vars for command provider. Values support ${VAR} expansion. |
|
[remediation] |
agent_command |
Shell command to invoke the agent | |
auto_merge_severities |
[] |
Severities where auto-merge flag is set | |
batch |
false |
Bundle all drifted docs into one agent call | |
agent_timeout |
300 |
Seconds before agent process is killed | |
agent_env |
{} |
Extra env vars passed to agent. Values support ${VAR} expansion. |
|
agent_instructions |
"" |
Replaces the default instructions in the agent payload |
|
[repos] |
git_timeout |
300 |
Seconds for clone/fetch/ls-remote operations |
[[repos.docs]] |
url |
Git URL of the documentation repository | |
path |
Subdirectory within docs repo for steering files | ||
ref |
Git branch or tag to track | ||
remote_name |
"origin" |
Git remote name for fetch operations | |
[[agents]] |
name |
Platform identifier (used with --agent flag) |
|
global_steering |
Path for symlinked steering files (dev machines) | ||
workspace_steering |
Path for copied steering files (CI) | ||
agents_file |
Platform-specific instructions file (e.g., AGENTS.md) |
||
skill_dir |
"" |
Path for agent skill files. If empty, skill installation is skipped. |
Configure timeouts in kedge.toml. Defaults fit within a 1-hour CI pipeline:
| Setting | Default | Worst case (5 docs) |
|---|---|---|
triage_timeout |
120s | ~10 min (serial) |
agent_timeout |
300s | ~25 min per-doc, ~5 min batch |
git_timeout |
300s | ~5 min (clone + fetch) |
Total worst case: ~40 min per-doc mode, ~20 min batch mode.
kedge auto-clones the docs repo from [[repos.docs]] in kedge.toml. CI pipelines only need the code repo checked out.
MR pipeline (gate on drift):
kedge-check:
stage: test
script:
- kedge check
variables:
KEDGE_CODE_REPO_URL: $CI_PROJECT_URL
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"Scheduled pipeline (full detect -> triage -> remediate -> MR):
kedge-update:
stage: docs
script:
- kedge install --workspace --group $KEDGE_GROUP
- kedge update --no-stamp
variables:
KEDGE_CODE_REPO_URL: $CI_PROJECT_URL
ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: always--no-stamp skips provenance writes for no_update anchors. Run kedge sync in the docs repo after agent MRs merge to advance provenance in a single commit.
name: Documentation Drift
on:
push:
branches: [main]
jobs:
kedge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install kedge
run: cargo install --path .
- name: Run drift detection and remediation
run: kedge update --no-stamp
env:
KEDGE_CODE_REPO_URL: ${{ github.server_url }}/${{ github.repository }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
If provider = anthropic |
API key for Anthropic triage calls |
OPENAI_API_KEY |
If provider = openai |
API key for OpenAI-compatible triage calls |
KEDGE_CODE_REPO_URL |
No | Override code repo URL (default: file://<cwd>) |
KEDGE_DOCS_PATH |
No | Use a local docs path instead of cloning from [[repos.docs]]. For local testing or monorepos. |
KEDGE_DOCS_REPO_URL |
No | Docs repo URL for agent payloads. Only needed with KEDGE_DOCS_PATH in a two-repo setup (default: code repo URL). With [[repos.docs]], the URL comes from config. |
Pass extra env vars to the agent process via agent_env in kedge.toml:
[remediation]
agent_command = "your-agent-command"
agent_env = { GITLAB_TOKEN = "${GITLAB_TOKEN}", DOCS_REPO = "[email protected]:platform/docs.git" }anthropic Direct Anthropic API. Set api_url to route through an enterprise proxy or API gateway.
openai Any OpenAI-compatible endpoint (Azure OpenAI, vLLM, etc.). Requires model. If api_url ends with /v1, kedge appends /chat/completions.
command Pipes the triage prompt to an external command via stdin. Set triage_command and, if needed, triage_env. The command must print a JSON array to stdout:
[
{"path": "src/Auth.java", "symbol": "Auth#validate", "severity": "minor"},
{"path": "src/Baz.java", "symbol": null, "severity": "no_update"}
]Each element needs path, symbol (string or null), and severity ("no_update", "minor", or "major").
none Skips classification. All drifted anchors are set to major and forwarded to the remediation agent. Use this when your agent handles severity decisions based on org-specific rules.
Markdown files with kedge: frontmatter, stored in the docs repository:
---
kedge:
group: payments
anchors:
- repo: "[email protected]:platform/services.git"
path: src/auth/AuthService.java
symbol: AuthService#validateToken
provenance: "sig:a1b2c3d4e5f67890"
---
# Authentication Token Validation
This document describes how token validation works...| Field | Description |
|---|---|
group |
Business unit grouping (used with --group flag to scope operations) |
anchors[].repo |
Git URL of the code repository this anchor points to |
anchors[].path |
File path within the code repository |
anchors[].symbol |
Optional. Specific symbol to track (e.g., ClassName#methodName) |
anchors[].provenance |
Content fingerprint (sig:...) or git SHA of last-known-good state |
Content-addressed provenance (sig: prefix) is the default. The fingerprint captures the AST structure at the anchored location:
- Whitespace/comment immune. Formatting changes don't trigger drift.
- Rebase/amend/squash safe. kedge computes fingerprints from code structure, not git history.
- Symbol-scoped. Tracks specific declarations (e.g.,
AuthService#validateToken), not entire files.
kedge link stamps initial provenance. kedge sync advances provenance without changing doc content.
Legacy SHA-based provenance (plain git commit hashes) requires git history traversal but kedge still supports it.
The agent receives a JSON payload on stdin and prints output to stdout. Two response formats:
Structured JSON (preferred):
{"mr_url": "https://gitlab.example.com/platform/docs/-/merge_requests/42", "status": "success"}For batch mode (batch = true):
{"mr_urls": ["https://gitlab.example.com/.../merge_requests/42", "https://gitlab.example.com/.../merge_requests/43"], "status": "success"}Plain text fallback:
kedge scans stdout for URLs starting with https:// or http:// and uses the first match as the MR link.
kedge has two independent workflows:
Drift pipeline. Run in your code repo to detect stale docs, triage severity, and invoke agents to open MRs.
| Command | Description |
|---|---|
kedge init |
Create a default kedge.toml in the current directory |
kedge check [--report <file>] |
Detect drift and output a report (exit 1 if drift found) |
kedge triage [--report <file>] |
Classify drift severity via AI (reads from stdin or file) |
kedge update [--report <file>] [--no-stamp] |
Full pipeline: detect, triage, invoke agent, open MR (exit 1 if agent fails) |
kedge status |
Show all anchors and their current state |
kedge link [files...] |
Stamp content-addressed provenance on doc anchors |
kedge sync [files...] |
Advance provenance without changing doc content |
Steering distribution. Copies or symlinks doc files from the docs repo into directories where agents read them. Independent of the drift pipeline. Use it to set up agent workspaces on dev machines or in CI before the agent runs.
| Command | Description |
|---|---|
kedge install |
Pull steering files from docs repo to agent directories |
--config <path> overrides the default config file (kedge.toml) on all commands.
| Flag | Description |
|---|---|
--group <name> |
Only install steering files for this business unit |
--agent <name> |
Target a specific agent platform (default: all configured) |
--link |
Symlink to global steering directory (dev machines) |
--workspace |
Copy to workspace steering directory (CI) |
--check |
Skip if already up to date (compare local vs remote HEAD) |
--recursive |
Include files from subdirectories within group/shared folders |
CI environments (CI, GITHUB_ACTIONS, or GITLAB_CI set) default to --workspace mode unless you pass --link.
AST fingerprinting (whitespace/comment immune, symbol-scoped):
| Language | Extensions | Symbol syntax |
|---|---|---|
| Java | .java |
ClassName#methodName |
| Go | .go |
FunctionName |
| TypeScript | .ts, .tsx, .js, .jsx |
ClassName#methodName or functionName |
| Python | .py |
ClassName#method_name or function_name |
| Rust | .rs |
StructName#method_name or function_name |
| XML | .xml |
(file-level only) |
Other file types fall back to SHA-256 content hashing. The fallback hashes raw content, so whitespace and comment changes register as drift.
- Git CLI calls use
--end-of-options separator - kedge validates anchor paths against path traversal
- kedge validates provenance values against git ref injection
- kedge validates repo URLs against injection and strips credentials from error output.
- kedge sets cache directories to
0o700permissions kedge installskips symlinks when traversing source directories- HTTP via rustls (no OpenSSL dependency)
cargo build # Debug build
cargo test # All tests
cargo test --test fingerprint_test # Single test file
cargo clippy # Lint
cargo fmt # Format