Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
717 lines
32 KiB
Markdown
717 lines
32 KiB
Markdown
# 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/<slug>.rs` | Rust `impl Capability` trait | `kei-capability check` binary at PreToolUse |
|
||
| Rust module `verifies/<slug>.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<dyn Capability> 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 <name> (stdin JSON, exit 0|2)
|
||
kei-capability verify <name> (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
|
||
└── <agent-id>/{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" # <category>::<slug> 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/<agent-id>` 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<dyn Capability>`.
|
||
|
||
```rust
|
||
// kei-agent-runtime/src/capability.rs
|
||
|
||
pub trait Capability: Send + Sync {
|
||
fn name(&self) -> &'static str;
|
||
|
||
/// PreToolUse gate. Called by `kei-capability check <name>` 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 <name>`.
|
||
/// 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<String, String>,
|
||
}
|
||
|
||
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<String> },
|
||
}
|
||
```
|
||
|
||
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<Vec<Regex>> = 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::<Vec<_>>();
|
||
VerifyResult::Fail {
|
||
reason: "cargo check --workspace FAILED — agent-local green ≠ integration green".to_string(),
|
||
detail: Some(tail.into_iter().rev().collect::<Vec<_>>().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 <agent-worktree>
|
||
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/<agent-id> main # from MAIN repo, not worktree
|
||
git apply <agent-diff> # apply agent's changes on clean main
|
||
cd <test-merge branch>
|
||
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/<name>.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 <task.toml>
|
||
# → writes <task-dir>/prompt.md
|
||
|
||
# Spawn agent with composed prompt + install gates + record ledger
|
||
kei-agent-runtime spawn <task.toml>
|
||
# → returns agent-id; background-task notification semantics
|
||
|
||
# Run all capability verify predicates against agent's return
|
||
kei-agent-runtime verify <task.toml> <worktree-path>
|
||
# → exit 0 if all hold, non-zero with report of violations
|
||
|
||
# One-shot helper: compose + spawn + verify
|
||
kei-agent-runtime run <task.toml>
|
||
```
|
||
|
||
Execution flow:
|
||
|
||
```
|
||
1. orchestrator writes task.toml
|
||
2. `kei-agent-runtime compose` → prompt.md
|
||
3. `kei-agent-runtime spawn` →
|
||
a. kei-ledger fork <agent-id>
|
||
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 <crate>` 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::deny-tools` | tools | ✓/✓/— | PreToolUse denies Edit/Write entirely (renamed from `tools::read-only` in v0.17; old name resolves via alias) |
|
||
| `tools::bash-allowlist` | tools | ✓/✓/— | PreToolUse:Bash denies unless command matches allowlist pattern (renamed from `tools::cargo-only-bash` in v0.17; old name resolves via alias) |
|
||
|
||
---
|
||
|
||
## Initial role inventory (phase 2 builds these 5)
|
||
|
||
| Role | Capabilities | Tools |
|
||
|---|---|---|
|
||
| `read-only` | tools::deny-tools + output::report-format + output::severity-grade | Read / Glob / Grep / WebFetch |
|
||
| `explorer` | read-only caps + tools::bash-allowlist (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 <name>` 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 <name>` 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/<category>/<slug>/`** | 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 <name>` 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::deny-tools`, `output::report-format`, `output::severity-grade` |
|
||
| `_manifests/kei-architect.toml` | `read-only` | `tools::deny-tools`, `output::report-format`, `output::severity-grade` |
|
||
| `_manifests/kei-security-auditor.toml` | `read-only` | `tools::deny-tools`, `output::report-format`, `output::severity-grade` |
|
||
| `_manifests/kei-validator.toml` | `read-only` | `tools::deny-tools`, `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/`.
|
||
|
||
## Orchestrator ergonomics — `prepare` command
|
||
|
||
`compose` emits a prompt, `spawn` writes `tasks/<id>/` on disk, `verify` runs on return. Between compose and spawn, the orchestrator needs to invoke Claude Code's `Agent` tool — which lives inside Claude Code, not in Rust. `kei-agent-runtime prepare` bridges that step: it parses a `task.toml` and emits every argument the Agent-tool call needs in one copy-paste-ready block.
|
||
|
||
```
|
||
kei-agent-runtime prepare <task.toml> [--kit-root .] [--format human|json|toml]
|
||
```
|
||
|
||
Human output:
|
||
|
||
```
|
||
=== AGENT SUBSTRATE v1 — PREPARED SPAWN ===
|
||
agent-id: <id>
|
||
subagent_type: <role-derived>
|
||
isolation: worktree
|
||
description: <role> agent <short>
|
||
|
||
--- PROMPT (copy into Agent tool `prompt` param) ---
|
||
<composed prompt content>
|
||
--- END PROMPT ---
|
||
|
||
on return:
|
||
kei-agent-runtime verify tasks/<id>/task.toml --worktree <path-from-harness>
|
||
(orchestrator harness returns worktree path in the task-notification)
|
||
|
||
ledger: running agent-id=<id> role=<role> parent=<parent-or-none>
|
||
```
|
||
|
||
`--format=json` and `--format=toml` emit the same `AgentInvocation` struct for scriptable wrappers (e.g. future `/spawn-agent` Claude Code skill).
|
||
|
||
### Role → Claude subagent_type mapping
|
||
|
||
Claude Code's `Agent` tool takes a `subagent_type` string. Roles map to subagent_type via an optional `claude-subagent-type` field on `[role]` in `_roles/<name>.toml`. If unset, the runtime falls back to defaults:
|
||
|
||
| Role | Default `claude-subagent-type` |
|
||
|---|---|
|
||
| `edit-local` | `code-implementer` |
|
||
| `edit-shared` | `code-implementer` |
|
||
| `explorer` | `Explore` |
|
||
| `read-only` | `critic` (override per-task for architect-flavour reviews) |
|
||
| `git-ops` | `NOT-SPAWNABLE` (never composed — `spawnable = false`) |
|
||
|
||
`isolation = "worktree"` is auto-set for `edit-local` and `edit-shared`; other roles default to no isolation.
|
||
|
||
### Non-spawnable refusal
|
||
|
||
`prepare` refuses roles with `spawnable = false` and cites RULE 0.13 in the error. `git-ops` is the only shipped example; the refusal keeps "who can do git" boundary visible both in the role manifest AND at invocation time.
|
||
|
||
### Contract
|
||
|
||
`prepare` does NOT write to disk (inspection helper) and does NOT touch the ledger DB (the "ledger row" field is a pretty-printed string for the orchestrator to verify before calling `kei-ledger fork`). `spawn` remains the disk-writing step; `prepare` is additive and read-only.
|
||
|
||
### `kei-capability fork` — clone a capability
|
||
|
||
`kei-capability fork <source> --as <new-name> [--kit-root <dir>]` copies an existing `_capabilities/<src-cat>/<src-slug>/` directory under a new `<cat>::<slug>` name and records lineage so downstream tooling can trace the fork back to its parent.
|
||
|
||
```
|
||
kei-capability fork policy::no-git-ops --as policy::no-git-ops-lax
|
||
```
|
||
|
||
Behaviour:
|
||
|
||
1. Both `<source>` and `<new-name>` must parse as `<cat>::<slug>` with each half matching the shared slug regex (`^[a-z][a-z0-9-]{0,63}$`); upper-case or path-traversal input is rejected before any filesystem write.
|
||
2. Target directory `_capabilities/<new-cat>/<new-slug>/` must NOT exist — fork refuses to clobber.
|
||
3. `capability.toml` is parsed, rewritten with `[capability].name = "<new-name>"` (and `category` set to `<new-cat>`), then augmented with a new `[lineage]` table:
|
||
|
||
```toml
|
||
[lineage]
|
||
fork_from = "<source-name>"
|
||
parents = ["<source-name>"]
|
||
creator = "<env KEI_CREATOR_ID or 'unknown'>"
|
||
created = "<ISO-8601 UTC at fork time>"
|
||
```
|
||
|
||
4. `text.md` is copied byte-identical — the operator is expected to edit it afterwards to reflect the fork's new semantics.
|
||
5. On success the CLI prints source→target, the new directory, the number of fields rewritten, and a next-steps hint reminding the operator to edit `text.md` and ensure `[gate].rust-module` / `[verify].rust-module` match the new slug.
|
||
|
||
Fork is local-only; no ledger row is written. It is an ergonomic shortcut for authoring a derived capability; the resulting files are still subject to the normal review + merge workflow.
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
## Layer E — Role expression (extends / relaxes)
|
||
|
||
Roles compose via three optional fields on `[capabilities]`:
|
||
|
||
```toml
|
||
[capabilities]
|
||
extends = "<parent-role-slug>" # optional — flattened first
|
||
required = ["cap-a", "cap-b"] # optional — appended after parent
|
||
relaxes = ["cap-c"] # optional — dropped from flattened list
|
||
```
|
||
|
||
Resolution order:
|
||
|
||
1. If `extends` is present, recursively resolve the parent and take its flattened `required` list.
|
||
2. Append every local `required` entry not already present (order preserved).
|
||
3. Remove every entry named in `relaxes`. If a relaxed cap wasn't inherited, a stderr warning is emitted (no-op, not an error).
|
||
4. Cycle detection — an `extends` chain that loops back to an already-visiting role raises an error naming the offender.
|
||
|
||
Shipped examples:
|
||
|
||
- `_roles/read-only.toml` — base, no `extends`
|
||
- `_roles/explorer.toml` — `extends = "read-only"`, adds `tools::bash-allowlist`
|
||
- `_roles/edit-local.toml` — base
|
||
- `_roles/edit-shared.toml` — `extends = "edit-local"`, `required = []`, `relaxes = []` (the SSoT relaxation rides on `task.scope.files-whitelist`, not on capability drop)
|
||
|
||
Consumers: `compose::compose_prompt`, `prepare::prepare`, `verify::load_role_capabilities`, `dna::Dna::compose` — all go through `role::resolve_role`.
|
||
|
||
---
|
||
|
||
## Layer G — DNA identity
|
||
|
||
Every `AgentInvocation` carries a `dna` string encoding the composition:
|
||
|
||
```
|
||
<role>::<caps-bitmap>::<scope-hash>::<body-hash>-<nonce>
|
||
```
|
||
|
||
Segments:
|
||
|
||
- **role** — role slug from `task.role`
|
||
- **caps-bitmap** — hyphen-joined 2-char codes from the resolved capability list (see `dna::CAP_CODES`)
|
||
- **scope-hash** — 4-char `SHA-256` prefix of canonicalised scope (sorted whitelist + denylist)
|
||
- **body-hash** — 4-char `SHA-256` prefix of `task.body.text`
|
||
- **nonce** — 4-char random hex (disambiguates re-runs of identical specs)
|
||
|
||
Example (edit-local task touching `kei-forge`):
|
||
|
||
```
|
||
edit-local::NG-FW-FD-CP-CG-TG-ND-RF::A7B2::C9F1-xa7c
|
||
```
|
||
|
||
Round-trip: `Dna::compose(task, resolved)` → `.render()` → `Dna::parse(s)` returns an equal `Dna`. `render_human` prepends `dna: …` to the printable block; `render_json` and `render_toml` emit it as a `dna` field.
|
||
|
||
### Ledger integration
|
||
|
||
`kei-ledger` schema v2 adds a nullable `dna TEXT` column plus `idx_agents_dna_prefix` (first 30 chars) for DNA-prefix lookup. `kei-ledger fork … --dna <string>` persists it; legacy calls without the flag leave the column NULL so pre-v2 callers keep working.
|
||
|
||
### Capability atom codes (stable table)
|
||
|
||
| Name | Code |
|
||
|---|---|
|
||
| `policy::no-git-ops` | `NG` |
|
||
| `scope::files-whitelist` | `FW` |
|
||
| `scope::files-denylist` | `FD` |
|
||
| `quality::constructor-pattern` | `CP` |
|
||
| `quality::cargo-check-green` | `CG` |
|
||
| `quality::tests-green` | `TG` |
|
||
| `safety::no-dep-bump` | `ND` |
|
||
| `output::report-format` | `RF` |
|
||
| `output::severity-grade` | `SG` |
|
||
| `tools::deny-tools` | `DT` |
|
||
| `tools::bash-allowlist` | `BA` |
|
||
|
||
Additions are allowed; removals are not. Unknown names render as `??` so missing entries are visible rather than silently dropped.
|