KeiSeiKit-1.0/docs/AGENT-SUBSTRATE-SCHEMA.md
Parfii-bot 329d7e2a4d feat(agent-substrate/phase-5): migrate 5 kit agents to role+task-spec — substrate v1 FULL
Final phase of agent substrate v1. 5 shipped agents now declare role at
manifest level; assembler expands role's capability text fragments into
the generated .md at a new `# AGENT SUBSTRATE — role <name>` section.
Non-migrated agents byte-identical (golden snapshots green).

Migrated agents:
- kei-code-implementer → edit-local (8 caps: no-git-ops + scope/* +
  quality/* + safety::no-dep-bump + report-format)
- kei-critic → read-only (tools::read-only + output::report-format +
  output::severity-grade)
- kei-architect → read-only
- kei-security-auditor → read-only
- kei-validator → read-only

_assembler/ extensions:
- manifest.rs: substrate_role: Option<String>
- assembler.rs: write_substrate() before blocks (backward-compat; no
  role = no substrate section)
- substrate.rs (new, 102 LOC): loads _roles/<name>.toml, iterates
  capabilities.required, reads _capabilities/<cat>/<slug>/text.md,
  joins with \n\n---\n\n separator
- validator.rs: substrate role existence + cap-text presence check
- tests/substrate_role.rs (4 tests): happy path, unknown role, missing
  capability text, byte-parity on non-migrated
- tests/regenerate_migrated.rs (ignored by default): regeneration gate

_templates/task-examples/ — 5 example task.toml per migrated agent
showing orchestrator the valid invocation shape.

docs/AGENT-SUBSTRATE-SCHEMA.md: Phase 5 row ticked ✓ + Migrated agents
subsection listing 5 agents with roles + pointer to examples.

tests/substrate_integration.sh: +8 Phase-5 assertions
- All 5 migrated .md files contain "# AGENT SUBSTRATE — role"
- kei-code-implementer.md contains "MUST NOT invoke git" (policy::no-git-ops)
- Every _templates/task-examples/*.toml parses as valid TOML
- cargo check --workspace still passes post-migration
- kei-agent-runtime compose works on edit-local-forge.toml example

Tests: assembler 40/40 (was 30, +4 substrate_role + +1 ignored regen),
kei-agent-runtime + kei-capability 37/37 preserved.

Deferred: remaining 7 non-core agents (cost-guardian, modal-runner,
fal-ai-runner, infra/ml-implementer, ml-researcher, researcher) migrate
in v0.24 wave.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 03:07:18 +08:00

561 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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