@dogik/agent-ctx
v0.3.0
Published
Deterministic multi-layer build engine that compiles engine-neutral agent context (.agent/) into Claude Code and Gemini CLI artifacts.
Downloads
352
Readme
agent-ctx
A deterministic, multi-layer build engine that compiles engine-neutral
agent context (.agent/) into artifacts for Claude Code and Gemini CLI.
You author one source of truth in .agent/. agent-ctx resolves the layers
(personal / org / project / repo), composes them into a single model, and
renders per-engine artifacts (CLAUDE.md, GEMINI.md, .mcp.json,
.claude/, .gemini/).
Why
- One source, many engines. Stop maintaining
CLAUDE.mdandGEMINI.mdby hand. Edit.agent/once. - Layered composition. Personal preferences, org standards, project context, and repo specifics compose with clear precedence.
- Deterministic. Same source ⇒ byte-for-byte same artifact. CI can gate on
--check.
Install
Distributed as a pinned npm package — never copy-pasted into a repo:
Distributed as a pinned public npm package — never copy-pasted into a repo:
npm install --save-dev @dogik/[email protected]It's a public scoped package, so no .npmrc or auth is needed to install. The
package exposes the same agent-ctx binary — only the package name is scoped.
Usage
Connect a repository (remote sources — the default)
Point a repo at its layers by git URL + ref in .agent/manifest.yaml; on
build agent-ctx clones each source at that ref and composes from it. No paths, no
flags — system git provides access (see Accessing private sources).
# .agent/manifest.yaml
extends:
- personal
- org: acme
- project: platform # inside the acme repo — needs no source
sources:
personal:
repo: https://github.com/octo-dev/agent-personal.git
ref: main
org:acme:
repo: https://github.com/acme-inc/agent-config.git
ref: v2026.06.1 # pin a tag/commit = reproducible build
targets: [claude, gemini]agent-ctx build . # clone sources at their ref, render artifacts, commit
agent-ctx build . --check # CI gate: exit 1 if artifacts would change, write nothing
agent-ctx status . # git ls-remote probe: is the committed artifact behind?
agent-ctx refresh . # re-fetch sources, rebuild, show the diff (no commit)
agent-ctx cache clear # drop the clone cache (an optimization, not state)Ready-to-copy example layouts:
examples/consuming-repo/ is a built workplace (its
manifest + the committed artifacts), and examples/sources/
holds the repos it points at — an org repo (with a project inside it) and a
personal repo.
Local override (develop a layer without pushing)
Resolve a layer from a local path instead of its source — the escape hatch while
iterating. A local path wins over the matching sources entry: orgs via a
registry.yaml, personal via --personal.
agent-ctx build . --registry ../agent-config/registry.yaml --personal ~/.agent
agent-ctx init . --org acme --registry ../agent-config/registry.yaml # scaffold + register dependent--registry, --personal, and --cache-dir also read from AGENT_REGISTRY /
AGENT_PERSONAL / AGENT_CACHE so pre-commit and CI can stay terse — all three
are optional.
Commands
build <place>— compose the layers and render artifacts. Remotesourcesare cloned/fetched into the cache at their pinned ref first.--checkwrites nothing and exits non-zero if anything would change (the CI gate).status <place>— compare the org version stamped in the committed artifact against the org's current upstream state. For a remote source this is agit ls-remoteprobe (no clone); reportsup to date/behind(exit 1) /no artifact/cannot check(no access — diagnostic, not a failure). Makes the manual update mode safe: a repo may lag, but the lag is visible on demand.refresh <place>— bring source layers to their current ref (remote sources are re-fetched into the cache by the rebuild; local-override git trees aregit pull --ff-only, deduped by top-level, the repo's own git untouched), rebuild, and print the diff. It does not commit — the human decides.cache clear— delete the source clone cache (--cache-dir/AGENT_CACHE). The cache is an optimization, never state: clearing it only forces a re-clone.init <place>— scaffold.agent/. With--org Xthe manifest extends that org and the repo registers itself in the org'sdependents.yaml(see Federated distribution).
Source layout (.agent/)
.agent/
manifest.yaml # extends + precedence + targets
*.md # context docs (layered into CLAUDE.md / GEMINI.md)
skills/<name>/
skill.yaml # name, description, disallowed_tools?
body.md
subagents/*.yaml # name, description, system_prompt, tools?
mcp/*.yaml # name, command?/url?, args?, env?
hooks/*.yaml # name, event, command (Claude-only)manifest.yaml
extends:
- personal
- org: acme
- project: platform
sources: # where each layer comes from
personal:
repo: https://github.com/you/agent-personal.git
ref: main
org:acme:
repo: https://github.com/acme-org/agent-config.git
ref: v2026.06.1 # pin a tag/commit = reproducible
# project:platform needs NO source — a project lives inside its org repo
# (orgs/acme/projects/platform) and arrives with the org clone.
compose:
precedence: [repo, project, org, personal] # highest-first
targets:
- claude
- geminisources maps each non-repo layer to a remote git repo and a ref. At build
time agent-ctx clones the pinned ref into a cache and composes from it — auth is
delegated entirely to system git (see Accessing private sources and INVARIANT
6). Pin ref to a tag/commit for reproducibility, or use a branch (main) to
track the tip; a branch is resolved to the concrete commit SHA stamped in the
artifact header.
registry.yaml (local override)
sources is the default. A local path override takes precedence over it —
the escape hatch for iterating on a layer without committing/pushing. Orgs are
overridden via the registry; personal via --personal. When a layer has a local
override, its source is ignored and no clone happens.
orgs:
acme: { path: ../orgs/acme }
globex: { path: ../orgs/globex }Composition rules
- Context docs accumulate from every layer in base→top order (personal → org → project → repo). Order is significant.
- Skills, subagents, MCP servers, hooks are keyed by name; the higher-precedence layer overrides a same-named entry (default: repo wins, personal is the base).
Invariants
These are non-negotiable and covered by tests:
- Source / artifact separation. Generated files (
CLAUDE.md,GEMINI.md,AGENTS.md, …) are never read back in as source. A stale generated file sitting in a source dir is skipped. - Engine holds no secrets — fail closed. Org isolation rests on access to the org's content. An unreachable org source aborts the build with a nonzero exit and writes nothing — it is never silently skipped.
- Determinism. No timestamps, no nondeterministic ordering. Rebuilding
unchanged source is a no-op. The artifact header carries a
source-hash, not a build time. - Multi-stage. A directory builds iff it holds
.agent/manifest.yaml. An org folder can be both a workplace and a pure source. - Capability warnings. When a target engine cannot enforce a feature
(Gemini has no hooks; Gemini cannot enforce skill
disallowed-tools), the adapter emits a text warning rather than silently dropping it. - agent-ctx never touches credentials (auth-delegation). It never stores,
reads, parses, forwards, or logs tokens. Access to private sources is handled
only by system git via its credential helper. agent-ctx merely spawns
git clone/git fetch/git ls-remoteas child processes and passes source URLs through verbatim — neverhttps://<token>@…. This is what lets the engine be open and run safely in any container/CI: there is no secret in it to steal. Multi-org isolation rests on the same fact — the environment holds credentials only for the orgs it may reach; no access → git fails → the build fails closed (INVARIANT 2). agent-ctx makes no access decisions; git and the environment do.
Capability matrix
| Feature | Claude | Gemini |
| -------------------- | ------------- | --------------------------------- |
| Context docs | CLAUDE.md | GEMINI.md |
| Skills | .claude/skills/<n>/SKILL.md | .gemini/commands/<n>.toml |
| Skill disallow-tools | enforced | warning (not enforced) |
| Subagents | .claude/agents/<n>.md | .gemini/settings.json agents |
| MCP servers | .mcp.json (url) | .gemini/settings.json (httpUrl) |
| Hooks | .claude/settings.json | warning (no equivalent) |
Federated distribution
When an org layer changes, dependent repos carry a stale artifact until rebuilt. Distribution is federated: org-CI only signals dependents that the org changed; each dependent's own CI does its own rebuild and commit (narrow local rights instead of broad central access). Automation is the repo's choice:
- automatic — catch the signal, rebuild, commit;
- semi-automatic — catch the signal, open a PR;
- manual — no signal; a human runs
status/refreshwhen they like.
The rebuild mechanics (agent-ctx build with access to the layers) are
identical everywhere; only the trigger differs. init --org X records the repo
in org X's dependents.yaml (a plain, sorted, non-secret list) so org-CI knows
whom to signal. The container that executes agent tasks consumes the already
committed artifact — it never runs agent-ctx or clones layers.
Accessing private sources
agent-ctx calls git clone / git fetch / git ls-remote; access is
configured entirely through your environment's git credential helper — never
through agent-ctx, which holds no tokens (INVARIANT 6). The binary is identical
in every environment; only the environment's git setup changes.
For multi-org setups, enable per-URL credential selection so git picks the right token by full repo URL rather than only by host:
git config --global credential.useHttpPath truePer environment (see git / GCM / gh / Coder docs for exact commands):
- macOS —
osxkeychain, or Git Credential Manager (Keychain-backed). - Windows / WSL — Git Credential Manager (Windows Credential Manager). From
WSL you can bridge to the Windows GCM (
credential.helper→git-credential-manager.exe). - Linux server (non-interactive) — a token in env from a protected
.env, org-level SSH deploy keys (don't expire), orgh auth logindevice-flow. - Coder — don't configure a helper by hand: Coder injects tokens via
GIT_ASKPASSfrom its external-auth providers (one per access source, declared in the template withdata "coder_external_auth", which also gates workspace start). Use HTTPS source URLs so Coder matches them to providers by URL. - GitHub CLI (
gh) — optional helper viagh auth setup-git; convenient on a server (device-flow) and in a worker (one token for both clone andgh pr create). Weaker for multi-org (built around one active account) and GitHub-only — handy, but not provider-neutral.
If a clone fails, agent-ctx aborts the build (fail closed) with the source URL and a hint to check your credential helper — never with any credential material.
Distribution & guardrails
The engine is published to the public npm registry and shared as a pinned dependency — never copy-pasted into a repo.
Releasing a new version (from this repo):
# keep src/version.ts in sync with package.json, then:
npm version 0.1.1 # bumps package.json + tags v0.1.1
git push --follow-tags # the tag triggers .github/workflows/publish.ymlConsuming repos get these drop-ins from examples/:
examples/pre-commit— rebuild +git addartifacts.examples/build-is-clean.yml— CI gate viabuild . --check.examples/gitattributes— mark artifactslinguist-generatedso they collapse in review.
Development
Setup
npm installRunning the CLI locally
You do not need to build to run the CLI. npm run dev executes the
TypeScript source directly through tsx, so edit src/ and run immediately.
Pass CLI args after a -- (npm forwards everything after it to the script):
npm run dev -- <command> <args> # e.g. init / build / status / refreshThe -- is required. Without it, npm eats the flags and the command silently
does nothing.
Safe scaffold into a throwaway dir (never touches your home or global
config — init only writes into the <place> you name, and is idempotent):
npm run dev -- init /tmp/agt-playground
# created: /tmp/agt-playground/.agent/manifest.yaml
# created: /tmp/agt-playground/.agent/context.mdFull multi-layer build against the bundled fixtures — a real
registry + personal + org + repo wired up under test/fixtures/, so you can see
composition end-to-end without inventing config:
npm run dev -- build test/fixtures/work/acme/api-service \
--registry test/fixtures/registry.yaml \
--personal test/fixtures/personalThis writes CLAUDE.md, GEMINI.md, .claude/, .gemini/, .mcp.json into
the fixture place. They land as untracked files — discard them with
git clean -fd test/fixtures/work/.
--registry / --personal also read from AGENT_REGISTRY / AGENT_PERSONAL,
so you can export them once and keep commands terse:
export AGENT_REGISTRY=test/fixtures/registry.yaml
export AGENT_PERSONAL=test/fixtures/personal
npm run dev -- status test/fixtures/work/acme/api-service
npm run dev -- build test/fixtures/work/acme/api-service --check # CI gateTip: keep all manual experiments under
/tmp/...ortest/fixtures/. Becauseinit/buildwrite only into the<place>argument, pointing them at a scratch dir means you can never clobber your real working tree.
Running the compiled binary
To exercise the published entrypoint (bin/agent-ctx → dist/cli.js) you must
build first:
npm run build # tsc -> dist/
node bin/agent-ctx init /tmp/agt-playgroundTest & typecheck
npm test # vitest run — isolated, uses test/fixtures, no global writes
npm run test:watch # vitest in watch mode
npm run typecheck # tsc --noEmitTests import modules directly (they never shell out to the CLI), so they need no build and have no side effects on your environment.
License
MIT
