Workspace & Lifecycle¶
keryx has exactly one product: the per-project .metaproject/ workspace — a
file-based "agent operating system" that materializes a repo's structure, quality,
tests, conventions, and history as durable, human-editable Markdown plus
machine-readable JSON. The CLI itself only performs deterministic mechanics
(scaffold, refresh, score, checksum, render); the "thinking" is delegated to the
agent skills the workspace ships.
This page is the contract for that directory: its layout, the source-of-truth vs
generated split, the metaproject.json manifest, the agent entrypoints, and how
init / update build and keep it fresh without ever destroying accumulated
project knowledge.
The .metaproject/ directory layout¶
.metaproject/
├── metaproject.json # MANIFEST — authoritative runtime config (see below)
├── index.md # agent entrypoint: module/rules/skills/workflow/data map
├── README.md # human-oriented workspace readme (seed-once)
├── llms.txt # deterministic llms.txt index of the workspace (agent-facing)
├── assets.lock.json # pinned asset registry — checksums for vendored assets (e.g. tree-sitter grammars)
├── keryx-dashboard.html # self-contained human dashboard (offline, file://-safe)
├── *.config.json # per-module config, seed-once: gdctx / health / testing / memory / security
├── core/ # vendored runtime scripts per module (e.g. gdgraph build/query/cli)
├── templates/ # scaffolded templates dir
├── data/ # GENERATED artifacts — NEVER written by init/update (see invariant)
├── rules/ # imported + distilled agent rules (source of truth)
│ └── entrypoints/ # distilled project rules + distilled index.md
├── skills/ # installed bundled skills (SKILL.md) — regenerated each run
│ └── project-rules/ # project-rules skill readme
├── project-skills/<m>/<n>/ # project skills — source of truth, human/agent-authored
│ └── entrypoints/<slug>/ # distilled skills (SKILL.md) from `rules distill`
├── wiki/ # knowledge-base pages (source of truth)
├── memory/ # typed memory entries — Markdown, source of truth
├── flows/<NNN>-<date>-<slug>/ # flow (task) packages; flow.json is CLI-owned state
├── workspaces/<id>/ # SAC primary records (workspace.json, proposals, activity.jsonl)
├── context-operations/ # SAC access-receipt ledger + optional policy-experiment config
├── reviews/<date>-<target>/ # standalone managed review packages
├── modules/ # per-module manifests + READMEs
├── hooks/ # hooks readme + post-update.d/ (executables run by `update --hooks`)
├── jobs/ # orchestration job folders (gdskills)
└── reports/ # scratch report output (gitignored)
The data/ subtree fans out per feature module — each module owns and writes only
its own subtree at runtime:
data/
├── gdgraph/{storage,artifacts,summaries,queries}/ # import/dependency graph (nodes/edges JSONL)
├── health/{artifacts,history,raw}/ # quality scores + baseline history
├── testing/{artifacts,history,logs,context.md}/ # normalized test reports
├── gdctx/{raw,artifacts,queries}/ # compacted git/rg/shell captures
├── gdwiki/{artifacts,link-check}/ # wiki link checks / collected drafts
├── gdskills/{artifacts,proposals,reports}/ # skill learn/verify outputs
├── security/{artifacts,incidents,policies,raw,redactions}/ # scan reports, incidents, redactions
├── tasks/{artifacts}/ # flow (task) run outputs
└── memory/{index,embeddings}/ # optional disposable memory catalog/cache
Source of truth vs generated data/ — the data-vs-service invariant¶
The central invariant of the whole system is a strict split inside .metaproject/:
- Service files — templates, manifests, skills, hooks, dashboard, config,
index.md. These are regenerated byinit/update, reconciled to the rendered template on every run. - Data artifacts — everything under
.metaproject/data/**. These are module run outputs and are NEVER written by the lifecycle commands. Each feature module writes only its owndata/<module>/subtree at runtime; the lifecycle layer treatsdata/as read-only and, after a refresh, explicitly reports "Data artifacts were left untouched."
This separation is what lets a self-update refresh the toolchain (new templates, new skills, new hook scripts) without destroying accumulated project knowledge (the graph, health history, test reports, wiki, memory, flows).
Source-of-truth trees (wiki/, memory/, project-skills/, rules/) are seeded
once by init or by module new/create commands, then owned by the human. The
tooling guards them: writeTextIfMissing seeds and never clobbers; gdwiki only
overwrites still-unmodified generated drafts; gdskills learn never mutates a
SKILL.md without an explicit apply; flow's flow.json is the CLI's exclusive
writer with an AC-checksum tamper check. Managed review manifests are likewise
service-owned; standalone packages live under reviews/, while attached packages
live under a flow's reviews/ subtree.
Versioned vs gitignored¶
init injects a managed block into the repo's root .gitignore (delimited by
# keryx:begin … # keryx:end, replacing any legacy .metaproject/
lines). The policy is: keep agent-facing context versioned, ignore
executable/generated internals. The current block:
# keryx:begin
# Metaproject: keep agent-facing context versioned, ignore executable/generated internals.
.metaproject/runtime/
.metaproject/core/**/*.ts
.metaproject/data/**/storage/
.metaproject/data/**/raw/
.metaproject/data/**/queries/
.metaproject/data/**/summaries/
.metaproject/data/gdctx/artifacts/
.metaproject/data/gdwiki/artifacts/
.metaproject/data/gdwiki/link-check/
.metaproject/data/health/history/
.metaproject/data/health/artifacts/latest.md
.metaproject/data/health/artifacts/latest.json
.metaproject/data/testing/history/
.metaproject/data/testing/logs/
.metaproject/data/testing/artifacts/latest.md
.metaproject/data/testing/artifacts/latest.json
.metaproject/data/tasks/runtime/
.metaproject/data/tasks/logs/
.metaproject/flows/.flow-init.lock/
.metaproject/flows/.flow-lock-*/
# Security: local-only HMAC key, self-protect state, and local hash report must never be committed.
.metaproject/data/security/raw/
.metaproject/data/security/raw/**
.metaproject/data/security/artifacts/latest.md
.metaproject/data/security/artifacts/latest.json
.metaproject/reports/
# keryx:end
| Ignored (not versioned) | Versioned (committed) |
|---|---|
runtime/ (the self-updating CLI clone) |
metaproject.json, index.md, README.md |
core/**/*.ts (vendored, re-copied on update) |
rules/, skills/, project-skills/, wiki/, memory/, flows/ |
data/**/storage, raw, queries, summaries |
data/** summaries/reports that aren't listed (durable, human-facing) |
gdctx/gdwiki artifacts/, gdwiki link-check/ |
the dashboard HTML and per-module manifests/READMEs |
health/testing history/, logs/, and latest.{md,json} |
|
reports/ |
The rule of thumb: raw and re-derivable outputs are ignored; the distilled, agent-facing narrative (wiki, memory, rules, skills, flows, manifest, index) is committed so a clone carries its context with it.
The metaproject.json manifest¶
metaproject.json is the single authoritative runtime config. A freshly-init'd
manifest records:
schemaVersion(currently1),name("<project>-metaproject"),createdBy: "keryx".standardVersion— the Metaproject Standard version the manifest targets (currently"0.1.0").profiles[]— the standard profiles the workspace declares, a subset ofminimal/agent/ci/full, derived from the enabled modules (computeProfiles).updatedAt— ISO timestamp of the last lifecycle write.paths{}— resolved workspace paths (root, etc.).agentEntrypoints{ root: string[], metaproject: ".metaproject/index.md" }— the discovered root entrypoint sources plus the workspace index (see below).modules{}— a map keyed by module id, one entry per module. Each entry carries:enabled— whether the lifecycle commands scaffold/refresh it (9 modules are optional and default on; the MCP server module is opt-in via--mcp).- per-module settings — e.g. gdskills stores
profile,skills,catalog,projectSkills, and aprojectSkillRegistry[]. hooks{ gitPostCommit?, prePush?, agent?, postUpdate }— which hooks this module installs (securityalso recordsagent→.claude/settings.json).commands[]— the module's canonical CLI subcommand list.
commands[] comes from MODULE_COMMANDS (single source of truth)¶
The commands[] arrays are never hand-written into the manifest. src/commands/
module-commands.ts holds MODULE_COMMANDS, one canonical subcommand list per
module id, and the helper moduleCommands(id) returns a fresh mutable copy.
Both init (buildManifest) and update (refreshServiceFiles,
writeRecoveredManifest, enableTasksInManifest) fill commands[] from exactly
this one place, and module-commands.test.ts enforces that the generated manifest
stays in sync with the routers.
Current canonical lists:
| Module id | Subcommands |
|---|---|
| gdgraph | build, query, affected, repomap |
| gdctx | status, diff, rg, read, run, show |
| gdwiki | status, new, collect, index, check-links, validate |
| gdskills | status, list, inspect, route, catalog, install, create, verify, learn, export, sync, contracts |
| memory | new, index, search, ingest, check, reflect |
| tasks | init, list, status, freeze, start, task, ac, implemented, complete, block, unblock, check |
| health | run, status, gate, sources, explain, baseline, trend |
| testing | init, analyze, run, status, context, explain, related, report |
| security | status, scan, check-input, check-output, redact, report, policy, incidents |
Naming skew to remember: the manifest key tasks corresponds to the CLI verb
flow (the flow command routes the tasks subcommand set), and the manifest key
gdwiki corresponds to the CLI verb wiki (legacy wiki manifest keys are
migrated forward on read).
Agent entrypoints and the managed routing block¶
AGENTS.md / CLAUDE.md at the repo root are the agent's front door. The rules
module keeps them in sync with the workspace so any agent that reads the root file
is routed through keryx tooling.
Sync (rules sync, also run by init/update). For each discovered root
entrypoint, syncAgentRules imports the file verbatim into
.metaproject/rules/<slug>.md (a high-priority "imported rule" mirror) and injects
an idempotent managed block, delimited by the sentinel <!-- keryx:index -->,
directly into the root file. Everything after the sentinel is regenerated;
everything before it (the human's own prose) is preserved. The block adds per-skill
routing policies (consult .metaproject/index.md and the module skills before
doing raw file/code work) for gdgraph, gdwiki, gdctx, gdskills, testing, memory,
and flow.
The block is self-healing: ensureMetaprojectReference migrates old policy
wording to the current phrasing, de-duplicates repeated policies, and
adds/removes the flow policy based on modules.tasks.enabled. New root files get
the full policy set written fresh from the renderAgentEntrypoint template.
Entrypoint discovery resolves realpath and de-duplicates symlinks (so an
AGENTS.md → CLAUDE.md symlink isn't imported twice); candidate order is
manifest-declared sources first, then AGENTS.md, agents.md, CLAUDE.md,
claude.md. The discovered list is persisted back to
agentEntrypoints.root and index.md is refreshed.
Distill (rules distill). For large monolithic entrypoints, distill runs sync
first, then splits each Markdown section and classifies it heuristically into:
- a project rule →
.metaproject/rules/entrypoints/<slug>.md(type: distilled-entrypoint-rulefrontmatter), - a procedural skill →
.metaproject/project-skills/entrypoints/<slug>/SKILL.md, - or root-only instructions that stay in the trimmed root file.
The root entrypoint is then rewritten to keep only global/personal always-on
instructions plus the managed block, and a distilled index
(.metaproject/rules/entrypoints/index.md) is written. index.md records
hasDistilledEntrypoints so the workspace map reflects whether distillation ran.
Lifecycle: init and update¶
Both lifecycle commands are idempotent and can be re-run any number of times. They share the managed-block / idempotent-writer mechanism that makes this safe:
writeTextIfMissing— seed once, never overwrite user edits.writeTextIfChanged/writeJsonIfChanged— managed files, always reconciled to the freshly rendered template (no-op when identical, so no needless churn).copyFileIfChanged— vendored runtime scripts.- Sentinel-delimited managed regions inside otherwise user-owned files —
# keryx:<id>:begin … :end(gitignore, git hooks) and<!-- keryx:index -->(agent entrypoints) — so regeneration replaces only the managed span and leaves surrounding human content intact.
What init creates¶
initCommand (src/commands/init.ts) bootstraps the workspace:
- Parse flags → per-module enablement. The 9 optional modules
(
gdgraph, gdctx, gdwiki, gdskills, health, testing, memory, tasks, security) default on (security via--no-security); the MCP server module is opt-in via--mcp.--no-<module>disables one;--yesskips prompts; otherwise it asks interactively (TTY-safe, defaults when piped). gdskills additionally prompts for an install profile. Under--yesall post-commit hooks default on; only the pre-push testing gate is off by default (opt-in even under--yes). - Scaffold the base dirs (
core, data, rules, skills, skills/project-rules, modules, reports, templates, hooks, hooks/post-update.d) plus per-enabled-module dirs derived from the config tables (WIKI_PAGE_TYPES→ wiki folders,MEMORY_TYPES→ memory folders, gdgraph storage/artifacts/summaries/queries, etc.). - Inject managed blocks — the
.gitignoreblock, andsyncAgentRulesseeds/ updatesAGENTS.md/CLAUDE.mdwith the routing block. - Per-module bootstrap — gdgraph copies vendored
build.ts/query.ts/types.tsintocore/gdgraphand renders a localcli.ts; gdskills runsinstallGdskills(profile); testing runsanalyzeTestingProjectonce (the only placeinitproduces analysis). - Install git hooks (see below) — idempotent managed blocks in
.git/hooks/*, a no-op if.gitis absent. - Write the manifest via
buildManifest(embeddingmoduleCommands(id)per module, preserving any existingprojectSkillRegistry), then write the managed docs —index.md, per-module manifests/READMEs, everyskills/<m>/SKILL.md, and the dashboard HTML. Seed-once files (rootREADME.md, core/rules READMEs) go throughwriteTextIfMissing.
Re-running init over an existing workspace updates managed files but never
clobbers seeded user files or anything under data/.
What update does¶
updateCommand (src/commands/update.ts) refreshes an existing workspace (it
errors if .metaproject/ is missing):
- Runtime self-update (unless
--skip-runtime):updateRuntimefinds the runtime git repo —.metaproject/runtime/keryx/.git(project) or$HOME/.keryx/keryx/.git(global) — and doesgit fetch --depth 1 origin mainthengit checkout --force FETCH_HEAD. The CLI updates the copy of itself it was launched from — the same shallow-fetch mechanisminstall.shuses. refreshServiceFiles— the core of update:- Manifest recovery / migration: if
metaproject.jsonis missing or unparseable, infer enablement from directory existence (inferManifestFromExistingMetaproject); if parseable,normalizeManifestmigrates legacymodules.wiki→modules.gdwiki. - Tasks backfill: if the
tasks/flow module is disabled and--no-taskswasn't passed, force-enable it — this upgrades pre-tasks workspaces created before the flow module existed. - Re-run
syncAgentRules, re-render all managed docs/manifests/SKILL.md files, re-copy gdgraph core scripts, re-runinstallGdskills(withcreateDataDirs: false), and re-write configs only viawriteTextIfMissing(never overwriting user config). - Reconcile without touching
data/:createServiceDirsre-creates only managed dirs for enabled modules — neverdata/dirs. - Reinstall hooks conservatively: each module's git hook is reinstalled only
if the manifest already records it (
modules.<m>.hooks.gitPostCommit/prePush); the dashboard post-commit hook is reinstalled if any module has a post-commit hook or the existing hook already contains a# keryx:block. - Rebuild the dashboard:
collectDashboardDatare-reads read-onlydata/snapshots (health/graph/testing/wiki/memory/docs) and re-renders the self-contained HTML. - Manifest write-back: a recovered/invalid manifest is rewritten via
writeRecoveredManifest(a leanerversion: 1, generatedBy: "keryx update"shape); a backfilled-tasks-only case usesenableTasksInManifestto surgically inject thetasksentry without disturbing other keys; every run callsupdateManifestAgentEntrypoints. - Report — heading "Refreshed service files", the note "Data artifacts were
left untouched", flags for a recovered manifest or backfilled tasks, and a
per-module status line. With
--hooks,runPostUpdateHooksexecutes every executable in.metaproject/hooks/post-update.d(sorted,access X_OK, inherited stdio); otherwise it hints to pass--hooks.
dashboard build | open (and bare dash, which defaults to open) delegate to
buildDashboard, which re-collects the data/ snapshots and writes the
self-contained HTML — again without touching data/.
Git hooks¶
init installs git hooks through installManagedHook, which no-ops when .git
is absent and otherwise idempotently injects a # keryx:<blockId>:begin … :end
block into .git/hooks/<post-commit|pre-push> (creating a #!/usr/bin/env sh
shebang if the file is new, chmod 0o755). The rendered hooks are deliberately
non-mutating staleness reminders that return 0 on every branch — the
exceptions being the opt-in testing pre-push gate (blocks on test failure) and the
opt-in security pre-push gate (blocks in enforced/ci mode).
| Hook | Trigger | Behavior | Default under --yes |
|---|---|---|---|
| gdgraph post-commit | post-commit | reminder that the import graph may be stale | on |
| gdwiki post-commit | post-commit | reminder that wiki drafts may be stale (installed with the gdgraph post-commit hook) | on (derived) |
| gdskills post-commit | post-commit | skill verify / staleness reminder | on |
| health post-commit | post-commit | reminder to re-run health | on |
| dashboard post-commit | post-commit | rebuild the dashboard (installed if any post-commit hook is enabled) | on (derived) |
| testing post-commit | post-commit | reminder to re-run tests | on |
| testing pre-push | pre-push | blocking test gate — fails the push on test failure | off (opt-in even under --yes) |
| security pre-push | pre-push | scans changed files with security scan; advisory (default) warns, enforced/ci block the push |
on (offered when security enabled; opt out --no-security-hook) |
The security pre-push block coexists with the testing pre-push block and any
user-authored content in .git/hooks/pre-push. --no-*-hook flags force any hook
off. On update, a hook is reinstalled only if the manifest already records it, so
the workspace never silently re-adds hooks the user removed.
Agent hook (.claude/settings.json)¶
The security module also offers a non-git, project-local Claude Code agent
hook at init (opt out with --no-security-agent-hook). Rather than a
.git/hooks/* block, installSecurityAgentHooks merges two hooks into
.claude/settings.json — UserPromptSubmit → security check-input --source
untrusted-external and PreToolUse(Write|Edit) → security check-output. The
merge is merge-safe: a _keryxManaged: "security-agent-hooks" sentinel
keeps re-install idempotent and preserves every pre-existing key and user hook.
Advisory by default. It is recorded in the manifest at security.hooks.agent and,
like the git hooks, refreshed by update only when already recorded.
Orientation and routing hooks¶
Two additional opt-in integrations are installed explicitly rather than by
init defaults:
keryx orient install-hook --runtime <id>injects a bounded project-root.metaproject/index.mdexcerpt plus the graph map and wiki index at turn start for Claude, Codex, or Cursor.keryx ctx install-hook --runtime <id>installs the gdctx routing guard, which blocks broad raw shell/search reads and recommends the boundedctxcommand.
Both installers are merge-safe, idempotent, and reversible through their matching
uninstall-hook commands. They modify only managed config entries or sentinel
blocks and preserve surrounding user settings.