KeiSeiKit-1.0/docs/AGENT-SUBSTRATE-SCHEMA.md
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
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.
2026-05-01 12:09:03 +08:00

32 KiB
Raw Permalink Blame History

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 execs 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

[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):

## 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>.

// 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):

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):

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

[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)

[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:

# 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:

    [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-warningscargo 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]:

[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.tomlextends = "read-only", adds tools::bash-allowlist
  • _roles/edit-local.toml — base
  • _roles/edit-shared.tomlextends = "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.