# KeiSeiKit Agent Substrate Schema v1 **STATUS:** Decisions resolved 2026-04-23 — see updated Decision log at bottom. LOCK active upon `AGENT-SCHEMA-LOCKED.md` commit. 3-week parallel phase window. **PURPOSE:** Sibling SSoT to `SUBSTRATE-SCHEMA.md`. That one decomposes code primitives (atoms). This one decomposes **agent invocations** (capabilities). **Motivation from substrate v1 orchestration pain:** across 7 agent spawns in audit+follow-up waves, the same friction recurred — 40% prompt boilerplate, self-reported green tests that broke at integration, scope violations surfacing only after merge. Fix: capabilities become **enforced triplets**, not suggestions in freetext prompts. --- ## Core concept: capability atom = triplet An **agent capability** is not a reusable text block. It is a **declarative bundle + Rust implementation** that gives every restriction meaning across three layers: | Artifact | Format | Who consumes | |---|---|---| | `capability.toml` | TOML declarative metadata (name, category, patterns, parameters) | kei-agent-runtime at compose + lint time | | `text.md` | Markdown prompt fragment | Agent (via LLM context) | | Rust module `gates/.rs` | Rust `impl Capability` trait | `kei-capability check` binary at PreToolUse | | Rust module `verifies/.rs` | Rust `impl Capability` trait | `kei-capability verify` binary at on-return | The two Rust modules live in `_primitives/_rust/kei-agent-runtime/src/` — one compilation unit, one registry, `cargo test` on all gates/verifies at once. Shell hooks are 3-line glue that `exec`s the binary. **The invariant:** if any of the four artifacts is missing or fails, the capability did not hold. Self-reported compliance is not trusted — verification runs via **worktree short-circuit → simulated merge** pattern (see §Verify execution below) after agent return, catching integration regressions before merge to main. --- ## File layout ``` _capabilities/ — DECLARATIVE artefacts (phase 1 writes these) ├── policy/ │ └── no-git-ops/ │ ├── capability.toml │ └── text.md ├── scope/ │ ├── files-whitelist/{capability.toml, text.md} │ └── files-denylist/{capability.toml, text.md} ├── quality/ │ ├── constructor-pattern/{capability.toml, text.md} │ ├── cargo-check-green/{capability.toml, text.md} │ └── tests-green/{capability.toml, text.md} ├── safety/ │ └── no-dep-bump/{capability.toml, text.md} ├── output/ │ ├── report-format/{capability.toml, text.md} │ └── severity-grade/{capability.toml, text.md} └── tools/ ├── read-only/{capability.toml, text.md} └── cargo-only-bash/{capability.toml, text.md} _roles/ — DECLARATIVE (phase 2 writes these) ├── read-only.toml ├── explorer.toml ├── edit-local.toml ├── edit-shared.toml └── git-ops.toml — documented; NOT spawnable (orchestrator-only) _primitives/_rust/kei-agent-runtime/ — BINARY (phase 3 writes this) ├── Cargo.toml ├── src/ │ ├── lib.rs — exports Capability trait + registry │ ├── main.rs — CLI: compose | spawn | verify | run │ ├── compose.rs — task.toml + role + capabilities → prompt.md │ ├── spawn.rs — Agent-tool invocation with composed prompt │ ├── verify.rs — worktree short-circuit → simulated merge │ ├── simulated_merge.rs — create temp branch + apply diff + run checks │ ├── registry.rs — &str → Box dispatch │ ├── gates/ — PreToolUse logic │ │ ├── mod.rs │ │ ├── policy_no_git_ops.rs │ │ ├── scope_files_whitelist.rs │ │ ├── scope_files_denylist.rs │ │ ├── safety_no_dep_bump.rs │ │ ├── tools_read_only.rs │ │ └── tools_cargo_only_bash.rs — 6 gates │ └── verifies/ — on-return logic │ ├── mod.rs │ ├── quality_constructor_pattern.rs │ ├── quality_cargo_check_green.rs │ ├── quality_tests_green.rs │ ├── safety_no_dep_bump.rs │ ├── scope_files_whitelist.rs │ ├── scope_files_denylist.rs │ ├── output_report_format.rs │ └── output_severity_grade.rs — 8 verifies └── tests/ _primitives/_rust/kei-capability/ — BINARY (phase 3) ├── Cargo.toml — depends on kei-agent-runtime └── src/main.rs — clap CLI: kei-capability check (stdin JSON, exit 0|2) kei-capability verify (env-driven, exit 0 or fail) hooks/ — 3-line shell glue (phase 4 ✓ shipped) ├── agent-capability-check.sh — `exec kei-capability check "$KEI_CAPABILITY_NAME"` — PreToolUse:Bash|Edit|Write, no-op when env unset, fail-open on missing binary └── agent-capability-verify.sh — orchestrator-driven post-agent: `exec kei-capability verify "$KEI_CAPABILITY_NAME"` with AGENT_ID/TASK_TOML/WORKTREE_PATH/MAIN_REPO/RUN_MODE env tasks/ — ephemeral, gitignored └── /{task.toml, prompt.md} docs/AGENT-SUBSTRATE-SCHEMA.md — this file docs/AGENT-ROLES.md — human-readable role matrix (generated from _roles/*.toml) docs/AGENT-SCHEMA-LOCKED.md — lock marker ``` --- ## Capability atom — `capability.toml` shape ```toml [capability] name = "policy::no-git-ops" # :: namespace category = "policy" # policy | scope | quality | safety | output | tools version = "1.0" description = "RULE 0.13 — orchestrator owns git, agent writes files only" rationale = "See ~/.claude/rules/orchestrator-branch-first.md" [restricts] # What this capability forbids. Runtime gate enforces. tool-patterns = [ # matched against tool_input.command '^git( |$)', '^gh (repo|api /repos)', ] tools-denied = [] # e.g. ["Edit", "Write"] for read-only [parameterized] # Is this capability instance-configurable per task? accepts = [] # e.g. ["files-whitelist"] for scope/* caps [text] path = "text.md" # relative to capability dir [gate] # Rust module path inside kei-agent-runtime — registry dispatches by capability.name rust-module = "gates::policy_no_git_ops" # or empty if capability has no gate (verify-only) event = "PreToolUse:Bash" # PreToolUse:Bash | PreToolUse:Edit|Write | PreToolUse:Agent severity = "block" # block (exit 2) | warn (exit 0 + stderr) | advisory (log only) bypass-env = "ORCHESTRATOR_META" # optional env var to disable [verify] rust-module = "verifies::policy_no_git_ops" # or empty if gate-only run-mode = "simulated-merge" # worktree | simulated-merge | both when = "on-return" # on-return | per-tool-call ``` **`run-mode` values:** - `worktree` — run predicate inside the agent's worktree (fastest; what the agent saw) - `simulated-merge` — orchestrator creates `test-merge/` branch off main, applies agent diff, runs predicate from there (catches integration regressions of the E1-jsonschema-class — see §Verify execution) - `both` — worktree first (fail-fast), then simulated-merge (integration guarantee). Default for `quality::*` capabilities. --- ## Capability `text.md` conventions - Imperative, second-person, short. - ≤ 200 words per fragment. - No overlap — if two capabilities say the same thing, extract into a shared one. - Fragment stands alone — composer concatenates multiple fragments with `\n\n---\n\n` separator; fragments must not reference each other. - Lead with the rule ("You MUST NOT X"), follow with the why ("because Y"). Example (`_capabilities/policy/no-git-ops/text.md`): ```markdown ## No git operations You MUST NOT invoke `git`, `gh repo`, `gh api /repos`, or any shell command that modifies git state. Orchestrator handles all git operations (commits, branches, pushes, rebases). If your task requires staging a change, describe it in the return file-list — the orchestrator will commit on your behalf. Bypass exists for orchestrator-meta agents only; it is not available here. ``` --- ## Capability trait contract (Rust) All gates and verifies implement the same trait, dispatched by string name. Registry in `kei-agent-runtime/src/registry.rs` maps `"policy::no-git-ops"` to `Box`. ```rust // kei-agent-runtime/src/capability.rs pub trait Capability: Send + Sync { fn name(&self) -> &'static str; /// PreToolUse gate. Called by `kei-capability check ` binary. /// Receives the hook JSON payload from Claude Code on stdin. /// Returns Allow / Deny{reason} / NotApplicable. fn check(&self, ctx: &GateContext) -> GateDecision { GateDecision::NotApplicable // default: no gate, verify-only } /// On-return verification predicate. Called by `kei-capability verify `. /// Receives task context (agent-id, worktree path, main repo, task.toml values). /// Returns Pass / Fail{reason}. fn verify(&self, ctx: &VerifyContext) -> VerifyResult { VerifyResult::Pass // default: no verify, gate-only } } pub struct GateContext<'a> { pub tool_name: &'a str, pub tool_input: &'a Value, pub task: &'a TaskSpec, // parsed task.toml pub env: &'a HashMap, } pub enum GateDecision { Allow, Deny { reason: String }, NotApplicable, } pub struct VerifyContext<'a> { pub agent_id: &'a str, pub task: &'a TaskSpec, pub worktree_path: &'a Path, pub main_repo: &'a Path, pub run_mode: RunMode, // Worktree | SimulatedMerge | Both } pub enum VerifyResult { Pass, Fail { reason: String, detail: Option }, } ``` Example implementation (`_primitives/_rust/kei-agent-runtime/src/gates/policy_no_git_ops.rs`): ```rust use crate::capability::*; use regex::Regex; use once_cell::sync::Lazy; pub struct NoGitOps; static GIT_PATTERNS: Lazy> = Lazy::new(|| vec![ Regex::new(r"(?m)(?:^|[;&|]|\s)git(?:\s|$)").unwrap(), Regex::new(r"(?m)(?:^|[;&|]|\s)gh\s+repo").unwrap(), Regex::new(r"(?m)(?:^|[;&|]|\s)gh\s+api\s+/?repos").unwrap(), ]); impl Capability for NoGitOps { fn name(&self) -> &'static str { "policy::no-git-ops" } fn check(&self, ctx: &GateContext) -> GateDecision { if ctx.tool_name != "Bash" { return GateDecision::NotApplicable; } if ctx.env.get("ORCHESTRATOR_META").map(|v| v == "1").unwrap_or(false) { return GateDecision::Allow; } let cmd = ctx.tool_input.get("command").and_then(|v| v.as_str()).unwrap_or(""); for pat in GIT_PATTERNS.iter() { if pat.is_match(cmd) { return GateDecision::Deny { reason: format!("RULE 0.13 — git operation blocked (pattern {})", pat.as_str()), }; } } GateDecision::Allow } } ``` Example verify (`_primitives/_rust/kei-agent-runtime/src/verifies/quality_cargo_check_green.rs`): ```rust use crate::capability::*; use std::process::Command; pub struct CargoCheckGreen; impl Capability for CargoCheckGreen { fn name(&self) -> &'static str { "quality::cargo-check-green" } fn verify(&self, ctx: &VerifyContext) -> VerifyResult { let run_dir = match ctx.run_mode { RunMode::Worktree => ctx.worktree_path, RunMode::SimulatedMerge => &ctx.simulated_merge_path(), RunMode::Both => unreachable!("runtime runs `both` as two sequential calls"), }; let out = Command::new("cargo") .arg("check") .arg("--workspace") .current_dir(run_dir.join("_primitives/_rust")) .output(); match out { Err(e) => VerifyResult::Fail { reason: "cargo invocation failed".to_string(), detail: Some(e.to_string()), }, Ok(o) if !o.status.success() => { let tail = String::from_utf8_lossy(&o.stderr).lines().rev().take(5).collect::>(); VerifyResult::Fail { reason: "cargo check --workspace FAILED — agent-local green ≠ integration green".to_string(), detail: Some(tail.into_iter().rev().collect::>().join("\n")), } } Ok(_) => VerifyResult::Pass, } } } ``` ## Verify execution — worktree → simulated merge The orchestrator runs verification in **two sequential passes** for `run-mode = "both"`: ``` Pass 1 — worktree (fail-fast) cd run capability.verify(RunMode::Worktree) if Fail → reject immediately, don't bother with pass 2 Pass 2 — simulated-merge (integration guarantee) git checkout -b test-merge/ main # from MAIN repo, not worktree git apply # apply agent's changes on clean main cd run capability.verify(RunMode::SimulatedMerge) if Fail → reject with regression report if Pass → safe to merge, orchestrator proceeds ``` Why both: agent's worktree passing doesn't mean merged-main passing. E1's jsonschema regression was green in worktree (no real atoms there) but broke main integration (real atom schemas triggered the 0.17→0.18 breaking change). Simulated merge catches this class **before** it lands on main. Implementation lives in `kei-agent-runtime/src/simulated_merge.rs` — creates a temp worktree via `git worktree add`, applies diff, runs verify, cleans up. --- ## Role — `_roles/.toml` shape ```toml [role] name = "edit-local" display-name = "code-implementer (local edit scope)" description = "Write code + run cargo check/test + emit report. No git, no workspace touches." [capabilities] # Ordered list — text.md fragments concatenated in this order required = [ "policy::no-git-ops", "scope::files-whitelist", "scope::files-denylist", "quality::constructor-pattern", "quality::cargo-check-green", "quality::tests-green", "safety::no-dep-bump", "output::report-format", ] [tools] # Tool allowlist — anything not in this list is denied allowed = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] # Bash further restricted by quality/tools atoms bash-patterns-allowed = ['^cargo( |$)', '^mkdir( |$)', '^rm -rf /tmp/'] [escalation] policy = "ask-via-return" # ask-via-return | orchestrator-notify | fail-fast ``` --- ## Task spec — `task.toml` shape (orchestrator writes per spawn) ```toml [task] role = "edit-local" agent-id = "abc123…" # allocated by kei-ledger fork parent-agent = null # or parent ID for nested [scope] # Parameterizes scope::files-whitelist + scope::files-denylist files-whitelist = [ "_primitives/_rust/kei-forge/**", ] files-denylist = [ "_primitives/_rust/Cargo.toml", "_primitives/_rust/Cargo.lock", "scripts/**", ".github/**", ] [verification] # Parameterizes quality/* caps cargo-check-crates = ["kei-forge"] cargo-test-crates = ["kei-forge"] test-count-min = 44 [output] # Parameterizes output/report-format report-fields-required = ["files-touched", "cargo-check", "cargo-test", "loc-delta"] [body] # Free-text task instructions, concatenated AFTER role capability fragments text = """ Replace shell-out with pure-Rust templating. … """ ``` --- ## Runtime execution contract `kei-agent-runtime` crate provides: ```bash # Compose prompt from task spec kei-agent-runtime compose # → writes /prompt.md # Spawn agent with composed prompt + install gates + record ledger kei-agent-runtime spawn # → returns agent-id; background-task notification semantics # Run all capability verify predicates against agent's return kei-agent-runtime verify # → exit 0 if all hold, non-zero with report of violations # One-shot helper: compose + spawn + verify kei-agent-runtime run ``` Execution flow: ``` 1. orchestrator writes task.toml 2. `kei-agent-runtime compose` → prompt.md 3. `kei-agent-runtime spawn` → a. kei-ledger fork b. install PreToolUse gates parameterized by task.scope c. Agent tool call with isolation=worktree + composed prompt 4. [agent executes] 5. `kei-agent-runtime verify` → a. run each capability verify.sh from MAIN repo (not worktree) b. collect all violations c. exit 0 if empty, non-zero with report 6. orchestrator decides: merge | reject + respawn | reject + rollback ``` --- ## Initial capability atom inventory (phase 1 builds these 10) | Name | Category | text / gate / verify | Core restriction | |---|---|---|---| | `policy::no-git-ops` | policy | ✓/✓/✓ | Block `git`, `gh repo`, `gh api /repos` | | `scope::files-whitelist` | scope | ✓/✓/✓ | PreToolUse:Edit\|Write denies paths outside whitelist; on-return git diff check | | `scope::files-denylist` | scope | ✓/✓/✓ | PreToolUse:Edit\|Write denies paths in denylist (overrides whitelist) | | `quality::constructor-pattern` | quality | ✓/—/✓ | On return: no file > 200 LOC, no fn > 30 LOC | | `quality::cargo-check-green` | quality | ✓/—/✓ | On return: `cargo check --workspace` from MAIN passes | | `quality::tests-green` | quality | ✓/—/✓ | On return: `cargo test -p ` passes, count ≥ task min | | `safety::no-dep-bump` | safety | ✓/✓/✓ | PreToolUse:Edit on Cargo.toml denies unless task opts in; on-return lock-diff check | | `output::report-format` | output | ✓/—/✓ | On return: parse report, assert required fields present | | `tools::read-only` | tools | ✓/✓/— | PreToolUse denies Edit/Write entirely | | `tools::cargo-only-bash` | tools | ✓/✓/— | PreToolUse:Bash denies unless command matches allowlist pattern | --- ## Initial role inventory (phase 2 builds these 5) | Role | Capabilities | Tools | |---|---|---| | `read-only` | tools::read-only + output::report-format + output::severity-grade | Read / Glob / Grep / WebFetch | | `explorer` | read-only caps + tools::cargo-only-bash (for `cargo check`) | + Bash-cargo | | `edit-local` | policy::no-git-ops + scope::* + quality::* + safety::no-dep-bump + output::report-format | + Edit / Write / Bash-cargo | | `edit-shared` | edit-local caps + permission for specified SSoT patterns | Same + SSoT paths | | `git-ops` | Documented-only, NOT spawnable (orchestrator holds this) | All | --- ## Decision log — resolved 2026-04-23 | # | Question | Decision | Rationale | |---|---|---|---| | 1 | Layout per capability | **Declarative bundle (`capability.toml` + `text.md`) + Rust modules in runtime crate** | Declarative artefacts live with capability; executable logic lives with its sibling capabilities in one Rust crate for shared tests + type safety | | 2 | Gate language | **Rust** via `kei-capability check ` binary; shell hook = 3-line `exec` glue | Type safety, unit tests, one compilation unit for all gates. Shell remains only as Claude-Code-hook-protocol adapter | | 3 | Verify language | **Rust** same binary, `kei-capability verify ` subcommand | Same reasoning. Cargo output parsing, LOC checks, diff analysis — all better in Rust | | 4 | Config format (capability.toml / role.toml / task.toml) | **TOML** | Consistent with Cargo ecosystem. YAML reserved only for locked atom `.md` frontmatter (immutable under atom substrate v1 lock) | | 5 | Capability ID separator | **`::`** | Consistent with atom IDs. Rust-native | | 6 | Capability path layout | **Nested `_capabilities///`** | Scales to 50+ capabilities, category browsability | | 7 | Text fragment max | **200 words per capability** | Agent context budget; forces atomicity | | 8 | Verify execution | **worktree short-circuit → simulated-merge** (default `both` for `quality::*`) | Catches E1-jsonschema-class integration regressions before main merge. See §Verify execution | **Locked values:** all 8 above. Breaking changes require explicit user revocation + all-phases sync. --- ## Phase plan (post-lock, parallel) | Phase | What | Depends on | Agent | Estimate | |---|---|---|---|---| | 0 | This schema + lock marker | — | me | 0.5 day ✓ | | 1 | Capability library — 10 × (`capability.toml` + `text.md`) = **20 declarative files** | phase 0 | 1 code-implementer | 1-2 days | | 2 | Role matrix — 5 `_roles/*.toml` + auto-gen `docs/AGENT-ROLES.md` | phase 0 | 1 code-implementer | 0.5 day | | 3 | `kei-agent-runtime` + `kei-capability` binaries — compose/spawn/verify CLI + 6 gate modules + 8 verify modules + registry + simulated-merge executor | phase 0 | 1 code-implementer | 5-6 days | | 4 ✓ | Hook wiring — `agent-capability-check.sh` + `agent-capability-verify.sh` 3-line glue + settings.json registration | phases 1+3 | 1 code-implementer | 0.5 day (shipped) | | 5 ✓ | Migration — 5 kit-shipped agents (code-implementer / critic / architect / security-auditor / validator) adopt role+task-spec invocation via new `substrate_role` manifest field | phases 1+2+3+4 | 1 code-implementer | 1 day (shipped) | **Phases 1, 2, 3 start in parallel immediately after lock** (different dirs, zero file overlap). Phase 4 depends on 1+3. Phase 5 depends on everything. Total wall-time with parallel phases 1+2+3: **~7-8 days from lock** (phase 3 is critical path). --- ## Integration with substrate v1 This schema is **additive** to locked `SUBSTRATE-SCHEMA.md`. The two SSoTs sit side by side: - `SUBSTRATE-SCHEMA.md` — how code decomposes into atoms (locked 2026-04-22) - `AGENT-SUBSTRATE-SCHEMA.md` — how agent invocation decomposes into capabilities (this doc) Cross-ref: agent capability `quality::cargo-check-green` verifies that atoms compiled; atom agents produced via `kei-forge` can themselves be invoked through `kei-runtime` (atom substrate) OR composed into role definitions (agent substrate). Eventually (post-both-locks): **agents compose atoms, atoms compose agents**. Symmetric substrates. --- ## Lock declaration Once this document is approved by the user and `docs/AGENT-SCHEMA-LOCKED.md` is committed, the capability-triplet shape + role shape + task-spec shape + runtime contract are **immutable for 3 weeks** (shorter lock than atom substrate because agent substrate is greenfield, expected revisions). Breaking changes during lock require: 1. Explicit revocation by user 2. All parallel phase agents paused 3. Lock marker amended with revocation reason 4. `kei-ledger` row: bypass reason + revocation timestamp Non-breaking additions (new capability atoms beyond the initial 10, new roles, new parameterized fields on existing capabilities) are allowed during lock. --- ## Migrated agents Phase 5 wired the 5 kit-shipped agents to role+task-spec invocation via a new `substrate_role` field on the manifest. The assembler reads the declared role, expands each of its capability `text.md` fragments, and emits them under a `# AGENT SUBSTRATE — role ` section placed immediately after `# ROLE` and before the first behavioural block. | Agent manifest | Role | Capabilities expanded | |---|---|---| | `_manifests/kei-code-implementer.toml` | `edit-local` | `policy::no-git-ops`, `scope::files-whitelist`, `scope::files-denylist`, `quality::constructor-pattern`, `quality::cargo-check-green`, `quality::tests-green`, `safety::no-dep-bump`, `output::report-format` | | `_manifests/kei-critic.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` | | `_manifests/kei-architect.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` | | `_manifests/kei-security-auditor.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` | | `_manifests/kei-validator.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` | Backward compatibility: the `substrate_role` field is optional. The 7 non-migrated kit agents (`kei-cost-guardian`, `kei-fal-ai-runner`, `kei-infra-implementer`, `kei-ml-implementer`, `kei-ml-researcher`, `kei-modal-runner`, `kei-researcher`) continue to assemble without change; a deferred v0.24 migration wave will promote them. Task-spec examples showing how the orchestrator invokes each migrated agent live under `_templates/task-examples/`. ## Deferred extension candidates (non-breaking post-lock) Capability atoms NOT in the initial 10 but good follow-up PRs (non-breaking additions during lock window): - `safety::no-mass-delete` — PreToolUse denies `rm -rf` on more than N files - `output::ledger-row-required` — verify agent emitted ledger row per RULE 0.12 - `quality::no-warnings` — `cargo build --workspace` with `-D warnings` - `scope::no-rule-edits` — denies edits to `~/.claude/rules/*.md` unless orchestrator-meta Role `git-ops` — documented in `docs/AGENT-ROLES.md` only; `_roles/git-ops.toml` has `spawnable = false` field. Orchestrator code refuses to spawn it. Exists for documentation of "who can do git" boundary. Task spec persistence: task.toml files are ephemeral (gitignored under `tasks/`). Ledger row includes spec-SHA so historical specs are recoverable from `kei-sage` archive if someone wants cold-storage replay.