Files
ironclaw/AGENTS.md
Benjamin Kurrek 8b32989d0a Guidance unification: one canonical home per fact, a measured loader story, and a gate that keeps it true (#7306)
* ci(guidance): add check-guidance.py — guidance must reference the tree that exists

Four mechanical drift classes become build failures: every repo path named
by agent guidance (root AGENTS.md/CLAUDE.md, crates/** AGENTS/CLAUDE/
CONTRACT/README, .claude/rules/*.md, .claude/skills/*/SKILL.md) must
resolve in the tracked tree; every rules/skills frontmatter paths: glob
must match at least one tracked file (the dead-trigger class that let
skills.md never fire); every crate directory appears in its family's
AGENTS.md crate table (the guidance half of check-target-tree.py); and
every crate has a README.md (measured 62/62, so it gates).

Extraction is designed against false positives: fenced blocks, placeholder
tokens, MCP method names, dated-correction (✎) lines, and
'check-guidance: path-ok' lines are not claims; resolution honors the
citation forms measured on the live tree (root-relative, doc-relative,
name-prefix, crate-qualified-by-context, module-relative within the citing
crate). KNOWN_MISSING is a shrink-only suppression table — a row whose
reference stops dangling fails the gate until deleted, and surviving rows
print as warnings every run.

Fails closed on unreadable files, unparseable frontmatter, broken crate
discovery, and near-empty scans (floor constants). Self-test in
test-check-guidance.py (23 cases, refusals first, real repository last),
wired beside check-target-tree.py in code_style.yml; the test planner
classifies all three paths as static-control (verified exit 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(guidance): consolidate crate-tier CLAUDE.md files; rename module specs to CONTRACT.md

Steps 2+3 of the guidance unification (docs/reborn/guidance-conventions.md):

- Rename the four Module Specs table specs CLAUDE.md -> CONTRACT.md (llm,
  filesystem, webui, composition), matching the identity/trust precedent.
  Charter gates repointed (llm module_charter, webui handlers_module_charter)
  and every live reference updated; pointer stubs left behind so tooling that
  loads CLAUDE.md still lands on the spec.
- Fold the nine substantive out-of-table CLAUDE.md files: wasm, mcp, sandbox,
  auth, assistant, trace_commons, extension_manager become AGENTS.md-canonical
  (gates repointed with pinned phrases kept verbatim: the wasm_sandbox_core
  arch pin, mcp module_charter, auth module_charter, assistant
  reborn_services_module_charter); network and secrets fold into their README
  Invariants sections and drop the crate guidance pair entirely.
- Mark with the convention's absence-claim annotation the five crate-tier
  lines grandfathered by check-guidance KNOWN_MISSING (llm CONTRACT.md x3,
  composition CONTRACT.md, hooks AGENTS.md) and mark trace_commons'
  prescribed tests/queue.rs mirror as prescriptive-future.
- tests/CLAUDE.md: replace the retired root Current-Limitations citation with
  the measured ironclaw_observability description.

End state: zero prose CLAUDE.md outside the Module Specs table at the crate
tier (the four ironclaw_agent_loop src/tests directory guides stay, same
footing as the tests-tree harness guides).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(guidance): unify the root pair — AGENTS.md canonical, CLAUDE.md adapter

Step 1: root AGENTS.md (198 lines) and root CLAUDE.md (286 lines) shared zero
identical lines — the forked-pair drift the guidance convention forbids at
crate level, live at the root. Root AGENTS.md is now the canonical
tool-neutral contract (build/run/debug commands, hard invariants including
the unified extension model and the credential_name/extension_name identity
rules, the Module Specs table — now uniformly CONTRACT.md and gaining the
existing ironclaw_trust/CONTRACT.md row — testing discipline, tree map,
discovery, change discipline; 152 lines). Root CLAUDE.md is an @AGENTS.md
adapter plus the genuinely Claude-specific tail: skills/rules index,
codebase-graph MCP recipes, and the REPL info!/warn! logging rule (51 lines).

Cut while merging, each measured against the tree: the v1 Job State Machine
(no such state machine exists under crates/), Current Limitations (stale —
the observability claim no longer matches the crate), the Skills System
section (.claude/rules/skills.md and the domain crate own it), Extracted
Crates, the re-derivable key-traits list, and the long channel-onboarding
narrative (now three lines pointing at crates/extensions/AGENTS.md and the
worked slack example).

Every live citation of the root pair's moved sections is repointed (crates/
routing map + README, the deslop-reborn command, types/type-placement rules,
skills/common/config crate docs, a loop_host doc comment). The git-ignored
.codebase-memory/artifact.json mention carries the absence-claim annotation
for the check-guidance KNOWN_MISSING handoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(guidance): make CLAUDE.md a symlink to AGENTS.md at every tier

The loader question is now measured, not assumed. Headless canary experiment
with a discriminating control: a symlinked nested CLAUDE.md's target content IS
injected when a file in that directory is read, an @AGENTS.md import inside a
nested CLAUDE.md also expands, and a nested AGENTS.md alone is NOT read. So one
uniform rule holds everywhere: wherever an AGENTS.md exists, CLAUDE.md sits
beside it as a symlink — same bytes, zero maintenance, no second document to
drift.

64 pointer stubs become symlinks. The four spec crates keep CONTRACT.md as
canonical; their AGENTS.md routes there, so the spec stays one hop away while
the working rules now auto-inject instead of costing a voluntary read.

Also reconciled check-guidance.py's shrink-only KNOWN_MISSING table: all 8 rows
deleted because the content pass fixed the underlying lines, and the three
absence-claims the gate then surfaced carry markers. The table is empty.

Caveat recorded for the convention: nested injection fires only below cwd, and
appears not to fire in subagent sessions — family docs must stand alone when
read deliberately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(guidance): amend the convention with measured loader mechanics and budgets

The first version made crate AGENTS.md canonical and CLAUDE.md a pointer, which
moved working rules out of Claude Code's auto-inject path. Records what was
measured instead: subtree CLAUDE.md injects lazily, symlinks and @imports both
carry content, nested AGENTS.md is not read natively, and injection does not
fire in subagent sessions — so every doc must stand alone when read deliberately.

Adds size budgets per tier, extends scope to .claude/rules and .claude/skills
(where the worst drift was), names check-guidance.py as the enforcement with its
suppression markers, warns that some guidance is test-parsed (including the
heading-shadowing trap), and adds the remove/rename checklist that mirrors add.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(guidance): composition keeps a real CLAUDE.md, not a symlink

The blanket symlink pass broke composition_root_embeds_no_prompt_content, and
the gate is right to refuse: its ownership walks do not follow symlinks, so
stepping over one would let it report clean on a subtree it never read. This
crate keeps a regular pointer file, with the reason written in the file so the
next person does not 'fix' the inconsistency back into a break.

The uniform alias rule now has two stated exceptions: the root (real file, it
carries a Claude-only tail) and composition (real file, this gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(guidance): enforce the CLAUDE.md alias rule; scope path-ok to the marked reference

The branch's central invariant — a `CLAUDE.md -> AGENTS.md` symlink beside
every AGENTS.md at the root and under crates/ — was unguarded: the audit
proved a committed symlink deletion left the gate green (a working-tree
deletion only tripped the accidental "cannot read guidance file" refusal).
Check 5 now judges the git index (`git ls-files -s` + `cat-file`): the
alias must be tracked, mode 120000, targeting exactly `AGENTS.md`. The two
real-file exceptions are named rows with reasons (the root adapter's
Claude-only tail; composition's symlink-refusing ownership walks), and a
row that stops matching the tree fails the gate rather than lingering.
Sabotage-verified on the real tree: `git rm --cached` on an alias went red
naming the pair; converting one to a tracked regular file went red;
restore went green (65 aliases verified).

Also from the audit:

- A `path-ok` marker now vouches for the one reference immediately
  preceding it instead of exempting its whole line — the audit slipped a
  fresh dangling path onto a marked line and passed. The `✎` glyph stays
  line-scoped by documented design. Both in-tree marker usages already
  sit marker-after-reference and keep working.
- Document the structural blind spot: a dead reference whose first
  segment died with its whole tree (the v1 `src/…` monolith) reads as
  historical narration and cannot be flagged; only review catches it.
- Re-measure the fail-closed floor comment — the shipped one claimed
  174 guidance files / ~800 references / 30 globs against a tree that
  measures 237 / ~2070 / 38 — and add a floor for alias-site discovery.

Self-test grows six cases: index-deleted alias, regular-file alias,
wrong-target alias, the load-bearing root exception row, exception rows
matching reality, and the marker-narrowing exploit. The `--tracked-files`
override marks symlinks as `<path> -> <target>`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(guidance): repoint dead skill refs, record alias carve-outs, honest size budgets

Content half of the guidance-unification audit fixes:

- architecture-video SKILL.md told readers to read `src/tools/README.md`
  and `src/workspace/README.md` — the v1 monolith is gone
  (`git ls-files | grep -c '^src/'` is 0) and the gate structurally
  cannot flag first-segment-dead paths. Repointed at the Reborn
  successors: `crates/extensions/AGENTS.md` and
  `crates/domains/ironclaw_memory/README.md`.
- guidance-conventions.md now records what only commit messages knew:
  the composition real-file exception beside the root one; the four
  sanctioned ironclaw_agent_loop sub-module CLAUDE.md guides; and the
  alias rule's actual scope (root + crates/**), naming the two
  out-of-scope AGENTS.md (docs/reborn/contracts, ironclaw_silk_decoder)
  instead of a "wherever" wording the tree contradicted.
- Size budgets re-derived from measurement (family <=220, crate <=160)
  with the four crate-tier exceptions named and reasoned. The shipped
  <=150/<=80 numbers were exceeded by 3 family and 24 of 54 crate docs
  on day one, which made the budget unreadable as a signal. No document
  was padded or truncated to fit.
- Root CLAUDE.md used the dated-correction glyph on the deliberately
  untracked `.codebase-memory/artifact.json` reference — suppression
  duty outside the glyph's documented historical-prose meaning. Swapped
  for `<!-- check-guidance: path-ok -->` beside the reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review(7306): CodeRabbit triage — guidance gate runs for the files it governs, brace globs, honest floors, identity-column family tables, doc-truth fixes

Trigger (Major, the inert-guard finding): fast-checks was gated on has_code,
whose regex covers none of .claude/, the root AGENTS.md/CLAUDE.md pair, or
docs/ — so a PR editing only a rule's paths: trigger skipped the gate built
for exactly that change. New has_guidance output OR-s those surfaces into
fast-checks only (clippy/JS lanes stay code-scoped); has_code keeps its
pinned meaning. Pinned by a ws12_workflow_contracts.py row and verified by
replaying representative change lists through the workflow's own extracted
EREs.

check-guidance.py: glob_to_regex now translates {a,b} brace alternation
(nested; unmatched braces stay literal) so a legitimate crates/**/*.{rs,toml}
trigger counts as live instead of being reported dead; MIN_RULE_GLOBS 1->20
and MIN_ALIAS_PAIRS 10->40 (~half of measured 38/65, so a degraded parser
refuses instead of passing); family-table coverage now requires the crate in
a row's identity (first) column — an incidental mention in another row's
prose no longer counts (measured 0 regressions on the live tree). Self-tests:
+3 (brace trigger end-to-end, duplicate KNOWN_MISSING rows, identity-column
regression) and the real-repository case documents its deliberate git
coupling. Floors sabotage-verified.

Doc truth, measured against code: composition CONTRACT — WS stream shares
SseCapacity (stream_events_ws try_acquire, pinned test) replacing 'No WS
surface to bound', webui_v2_app returns Result<Router, WebuiServeError>;
llm CONTRACT — the circuit breaker wraps failover (apply_decorator_chain
order), not the reverse; filesystem CONTRACT — dependency rule now names the
real manifest set (+libsql_runtime, +observability); extension_manager
AGENTS — the loops layer flip landed (layer = "loops"); four stale 'has no
CLAUDE.md' claims updated for the new symlink aliases (config, common,
event_store x2); root AGENTS — clippy line gains -- -D warnings (CI denies
warnings; unflagged clippy exits 0 with them) and the error bullet routes to
.claude/rules/error-handling.md; assistant/webui validation sections document
the real lane structure (self-dev-dep unifies test-support on, so the missing
shape is the no-dev-deps production lane, the #7119 class).

Stale pre-family paths in .rs prose: 594 crates/ironclaw_* citations
measured; 130 sit in comments, of which 106 repointed to their family homes
(every rewritten path verified to resolve), 10 of those needed deeper
repoints (files that moved crates: capability_host.rs, channel_pairing.rs,
approval_store_contract.rs, secret_store.rs, loop_contracts
instruction_bundle.rs, assistant communication_context.rs, loop_host
surface_disclosure.rs, resolver_tests.rs), 24 left deliberately (flat-
spelling narration about the family move itself, deleted-crate history,
synthetic fixture names, and two #6945-class pointers whose target is gone
at every spelling). 464 string-literal citations left: the specificity
test resolves legacy spellings through the crate inventory by design.

Triage of PR #7306 review comments; no gate weakened, both alias
exceptions preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review(7306): drop the one comment repoint in tests/e2e_trace_runtime_policy_org_ceiling_yolo.rs

reborn_pr_test_plan.py has no mapping for this root test (it matches
neither the tests/reborn_* partition inventory nor any other arm), so ANY
PR touching it fails 'Detect Reborn test scope' — a pre-existing planner
gap, confirmed against origin/main with a one-file changed list. The stale
crates/ironclaw_runtime_policy comment path inside it stays until the
planner learns the file; noted for follow-up rather than smuggling planner
surgery into a review-triage branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(contributing): stop annotating the loose iteration clippy line as 'zero warnings'

Same class as the root AGENTS.md fix: unflagged clippy exits 0 with
warnings, so the annotation overclaimed. CONTRIBUTING's two-tier design
(loose iteration block, then a stricter pre-PR block that already carries
-- -D warnings) is deliberate and stays; only the claim is aligned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: recount the frozen WebUI route table after the #7306 merge — 93 -> 97

#7236 (main) added the four operator inspector routes without bumping the
stated counts; re-derived on the merged tree:
rg -c 'pub const WEBUI_V2_ROUTE_' crates/product/ironclaw_webui/src/webui_v2/descriptors.rs -> 97.
Updates the two live claims (webui README, PROPOSAL SS6.9.4 with its
strike-through recount convention); historical/superseded 92-row mentions
stay as written.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): the code-style roll-up must judge fast-checks for guidance-only PRs

The has_guidance trigger made fast-checks RUN for .claude/ and root-pair
changes, but the roll-up's has_code==false branch exits 0 before it ever reads
fast-checks' result — so check-guidance.py could fail and Code Style would
still report success. The gate ran and could never block: exactly the
inert-guard shape this change exists to remove, reintroduced one layer up.

Fixed the way main's docs-publication gate already does it — judged before the
early exit, with the reason in a comment. Sabotage-verified: has_guidance=true
plus fast-checks=failure now exits 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:18:15 +00:00

17 KiB

Agent Rules

Purpose and precedence

AGENTS.md is the canonical agent contract for this repository — the commands, hard invariants, and routing an agent cannot infer from the tree. It is not the full architecture specification: before changing a complex area, read the owning crate's AGENTS.md, then its CONTRACT.md or README.md when present; cross-crate behavior is specified under docs/reborn/contracts/. (CLAUDE.md files are Claude Code adapters and pointer stubs; content lives here and in the files this one names.)

All product work belongs in the Reborn workspace under crates/; the shipping binary is ironclaw from the ironclaw package in crates/app/ironclaw_cli. crates/AGENTS.md is the routing map into the ten crate families. The repo skills under .claude/skills/ (ironclaw-reborn-orientation, reborn-feature, ironclaw-reborn-architecture-review, ironclaw-reborn-testing, ironclaw-reborn-skill-maintainer, reborn-extension-surfaces) are plain Markdown — read the SKILL.md directly if your harness does not load Claude skills.

Build, run, debug

cargo fmt                                                       # format
cargo clippy --all --benches --tests --examples --all-features -- -D warnings  # lint (zero warnings; CI denies warnings — an unflagged run exits 0 with them)
cargo test                                                      # unit + integration suites (Postgres legs self-provision testcontainers; skipped without Docker)
RUST_LOG=ironclaw=debug cargo run -p ironclaw -- serve          # run the serve binary (add tower_http=debug for HTTP logging)

The workspace-root integration feature is empty with zero consumers — a bare root cargo test --features integration adds nothing. Backend-heavy gated suites are crate-level (e.g. cargo test -p ironclaw_hooks --features integration,test-support). E2E suite: tests/e2e/CLAUDE.md.

Cargo features are a last resort. A feature is a second build of the workspace, compiled and tested forever. Add one only for a heavy optional dependency, a build shape that ships with it OFF, a CI lane selector, a dev-only seam (always named test-support), or a privilege boundary — and say which in the manifest comment. Deployment shape belongs in DeploymentConfig and [storage], not #[cfg]. Full bar: .claude/rules/cargo-features.md.

Discover code before changing it

For where-is, who-calls, data-flow, and impact questions, probe the codebase knowledge graph before text search: run bash scripts/codebase-graph.sh status once; if fresh and graph tools are connected, use them; otherwise fall back to crates/AGENTS.md, crate-local guidance, and targeted rg. Verify graph claims against live code before acting. Use rg directly for configuration, prose, and fixtures. openwiki/ is generated prose — read-only, never hand-edit.

Where work belongs

External surfaces normalize untrusted requests through product adapters or ProductSurface; thread/turn services establish durable conversation state; the scheduler and run executor invoke the canonical runner/driver and agent loop; capability execution crosses authorization, approvals, obligations, host-runtime mediation, and the selected runtime lane; durable typed events feed projections and transport streams — transports do not invent state. Verify a flow from live symbols:

rg -n "SessionThreadService|TurnCoordinator|TurnRunScheduler|RebornTurnRunExecutor|CanonicalAgentLoopExecutor|CapabilityHost" crates

Crates live under a family directory (crates/<family>/ironclaw_*); enumerate them with python3 scripts/ci/lib/crate_tree.py . rather than assuming a fixed depth. Stable ownership decisions:

  • Neutral authority vocabulary belongs in ironclaw_host_api; execution does not.
  • Filesystem mounts/CAS belong in ironclaw_filesystem; record grammar in the domain crate.
  • Durable events, projections, and transport streams are separate contracts.
  • Authorization, approvals, resources, obligations, dispatch, and runtime lanes remain separate stages.
  • ironclaw_assistant owns product-facing orchestration and ProductSurface; composition wires dependencies; WebUI owns HTTP/transport and frontend presentation.
  • Provider-neutral model contracts and provider implementations belong in ironclaw_llm; wrappers delegate the complete provider trait.
  • Declarative extension metadata belongs in ironclaw_extension_registry; execution belongs in runtime lanes and host mediation.
  • Safety scanning is ironclaw_safety; skills are ironclaw_skills; persistent memory is ironclaw_memory (model tools ironclaw.memory.*). Always import from the owning crate.

The composition root assembles dependencies; it does not own domain policy — module-specific initialization stays behind factories or builders in the owning crate. If adding a dependency would point from a lower neutral crate into product or composition, stop and run cargo test -p ironclaw_architecture_tests first.

Subagent spawn creates and wires child runs only; planning, execution, capability calls, checkpointing, gates, retries, and completion continue through the existing runner/driver/executor path.

Host-trusted trigger ingress is sealed by trigger-worker-owned request minting and private conversation-owned trusted construction. Product adapters, product workflow, first-party capabilities, and host-runtime handlers use untrusted inbound requests and must not mint TrustedInboundTurnRequest or call trusted trigger submitter factories.

Module Specs

When modifying a module with a spec, read the spec first. Code follows spec; spec is the tiebreaker.

Module Spec
crates/domains/ironclaw_llm/ crates/domains/ironclaw_llm/CONTRACT.md
crates/substrates/ironclaw_filesystem/ crates/substrates/ironclaw_filesystem/CONTRACT.md
crates/product/ironclaw_webui/ crates/product/ironclaw_webui/CONTRACT.md
crates/app/ironclaw_composition/ crates/app/ironclaw_composition/CONTRACT.md
crates/domains/ironclaw_identity/ crates/domains/ironclaw_identity/CONTRACT.md
crates/kernel/ironclaw_trust/ crates/kernel/ironclaw_trust/CONTRACT.md
tests/ (scenario coverage map) tests/CLAUDE.md
tests/integration/ tests/integration/CLAUDE.md
tests/support/reborn_parity_qa/ tests/support/reborn_parity_qa/CLAUDE.md
tests/e2e/ tests/e2e/CLAUDE.md

Coding and contract rules

  • No .unwrap() or .expect() in production code (tests are fine); propagate errors with context — .map_err(|e| SomeError::Variant { reason: e.to_string() })? — and use thiserror for error types in error.rs. Cause-preserving constructors, the map_err(|_| …) ban, and the other silent-failure anti-patterns: .claude/rules/error-handling.md.
  • Keep clippy clean with zero warnings. Prefer crate:: imports for cross-module references.
  • Use strong types and enums for known domain shapes; raw strings belong at external boundaries. Shared types live with the contract owner — no mirror DTOs, and ironclaw_common is not a dumping ground.
  • No pub use re-exports unless exposing to downstream consumers.
  • Prompt templates live in files, not Rust code: multi-line prompt strings go in a prompts/*.md file inside the crate that owns the behavior, loaded via include_str!() (ls -d crates/*/*/prompts crates/extensions/packages/*/prompts lists the owners). Single-line format strings are fine inline.
  • Preserve existing defaults unless the task explicitly changes them.
  • All I/O is async with tokio; use Arc<T> for shared state.

Testing discipline

  1. Test-first. Every feature and fix starts in the tests — pin the behavior, watch it fail for the right reason, then change the implementation. Every fix ships with a regression test.
  2. Consolidate, don't proliferate. Extend the test that already exercises the path; add a new test only for a genuinely distinct scenario.
  3. Integration-first. Production-wired behavior ships with a test in tests/integration/, driven through the harness and asserting at a seam — never wait_for_status(Completed) alone. Crate tier is the fallback only when that tier cannot reach the path (say why in the PR).
  4. Test through the caller, not just the helper. When a helper gates a side effect, unit-testing the helper alone is not regression coverage — drive the call site at the integration tier or higher, and make mocks capture every argument the production caller passes.

Full rules and tiers: .claude/rules/testing.md; authoring guides: tests/integration/CLAUDE.md, tests/e2e/CLAUDE.md. Select tiers with docs/internal/testing-playbook.md, and complete the Test Strategy section of .github/pull_request_template.md with evidence or Not applicable: <reason> per tier.

Persistence and configuration

New persistence uses RootFilesystem/ScopedFilesystem and the mount catalog owned by ironclaw_filesystem (spec above); composition chooses concrete backends (PostgreSQL, libSQL, local filesystem) by profile. Domain stores are thin typed wrappers and never branch on backend; keep dual-backend parity via shared conformance suites (.claude/rules/database.md). Read-modify-write uses the shared bounded CAS helper, never a process-local mutex held across backend I/O.

Keep bootstrap configuration, persisted settings, and encrypted secrets as separate layers; preserve configuration precedence, secret-mediated provider resolution, and fail-closed startup. Environment variables are documented in .env.example; LLM backends in the llm spec (LlmBackendKind in crates/domains/ironclaw_llm/src/config.rs is the source of truth).

Security and runtime invariants

  • Treat every listener, route, product adapter, runtime lane, container, and external service as untrusted until a typed boundary establishes otherwise.
  • Do not weaken authentication, origin checks, body limits, rate limits, allowlists, approval leases, secret mediation, or redaction guarantees.
  • External HTTP goes through ironclaw_network; credentials remain host-side and are injected only through mediated runtime services.
  • New ingress must validate and bound the original payload before persistence, prompt construction, credential injection, or dispatch.
  • Authorization, approval, reservation, dispatch, and execution are distinct stages. Do not bypass or collapse them — product/WebUI handlers, triggers, channels, and agent callers go through ProductSurface and the capability contracts, never around them to mutate stores directly.
  • Session, thread, turn, and run identities are typed and must not be re-derived from display strings or transport metadata.
  • LLM data is never deleted. Context, reasoning, tool calls, messages, events, steps — mark with timestamps and make filterable, but always retain. In-memory maps are caches; the database is the source of truth. "Cleanup" means evicting caches, never deleting rows.
  • Never commit secrets or PII.

Capabilities, extensions, and lifecycle

  • Core host behavior uses typed built-in capabilities behind the same mediated host surface as other execution.
  • Sandboxed extension execution belongs in WASM or a runtime lane; external server integrations belong behind MCP and the network boundary.
  • Discovery is side-effect-free. Installation, credential binding, activation, execution, deactivation, and removal are explicit lifecycle transitions.
  • Capability failures the model or user can correct are model-visible outcomes; host errors are reserved for failures that end the run.
  • Side-effecting success requires durable or provider-issued evidence plus read-back verification; if read-back is impossible, report explicitly unverified rather than completed.

Extension/Auth Invariants

The top-level product object is always an extension; a channel is one capability surface an extension's manifest declares (tool / channel / authironclaw_extension_contracts::surface::CapabilitySurfaceKind), and runtime (wasm / mcp / first_party) is implementation, never taxonomy. ExtensionId is the product identity (slack, github, gmail); VendorId (manifest field vendor) is the credential-authority namespace and may back several extensions (google backs gmail + drive + calendar). There is no separate channel registry and no extension kind wire string — crates/app/ironclaw_architecture_tests/tests/reborn_retired_taxonomy.rs pins the retired vocabulary at zero.

Two identities must never be conflated (newtypes in crates/contracts/ironclaw_common/src/identity.rs; identity model crates/domains/ironclaw_identity/CONTRACT.md; OAuth transport crates/domains/ironclaw_auth):

  • credential_name — backend secret identity (storage, injection, gate resume), e.g. telegram_bot_token, google_oauth_token.
  • extension_name — user-facing installed extension/channel identity (setup routing, UI), e.g. telegram, gmail.

Never route setup/configure UI from credential_name; chat and Settings use the same setup path; generic auth-card UI is only for non-extension credential prompts or pure OAuth launches; resolve extension_name once in shared backend logic and carry it through the wire contract instead of re-deriving it per layer or adding frontend-only fallbacks.

Adding a channel means adding one capability surface of an extension — a [channel] section in the reborn.extension_manifest.v3 manifest plus a ChannelAdapter (crates/contracts/ironclaw_extension_contracts/src/channel_adapter.rs), wired through RebornHostBindings::with_channel_extension_bindings (crates/app/ironclaw_composition/src/input.rs) — never per-channel host code. Start from the reborn-extension-surfaces skill; the worked example is crates/extensions/packages/slack/; family rules in crates/extensions/AGENTS.md.

Project structure

crates/                     # all production code, by family (crates/AGENTS.md is the map)
├── app/                    # ironclaw_cli (binary `ironclaw`), ironclaw_composition, ironclaw_config, ironclaw_architecture_tests
├── contracts/              # ironclaw_host_api, ironclaw_common, ironclaw_extension_contracts, ironclaw_product_contracts, …
├── domains/                # ironclaw_llm, ironclaw_skills, ironclaw_threads, ironclaw_auth, ironclaw_memory, …
├── events/                 # ironclaw_event_log / _projections / _store / _streams
├── extensions/             # ironclaw_extension_host/_manager/_registry/_support + packages/ (slack, telegram, …)
├── kernel/                 # ironclaw_turns, ironclaw_capabilities, ironclaw_approvals, ironclaw_host_runtime, …
├── lanes/                  # ironclaw_wasm, ironclaw_sandbox, ironclaw_mcp
├── loop/                   # ironclaw_agent_loop, ironclaw_turn_runner, ironclaw_loop_host, ironclaw_hooks
├── product/                # ironclaw_webui (SPA in frontend/), ironclaw_assistant, …
└── substrates/             # ironclaw_filesystem, ironclaw_safety, ironclaw_network, ironclaw_secrets, …

tests/                      # root-package integration suite, parity/QA, support, e2e

The workspace root (Cargo.toml, package ironclaw_integration_tests) hosts only the integration test suite; the one workspace exclude is tools/ironclaw_silk_decoder.

docs/ is the public Mintlify site plus fenced internal material. All new internal engineering docs (design notes, research, plans, QA maps) go under docs/internal/ — nowhere else under docs/. A page outside the docs/.mintignore fence is published even when omitted from docs.json navigation (hidden pages stay reachable by URL), and .mintignore is frozen: do not add entries. Enforced by scripts/ci/docs_publication_boundary.py (Code Style workflow); run it to check placement.

Change discipline, and before finishing

  • Keep changes scoped; preserve unrelated work in dirty worktrees; avoid generated-file churn. Security, persistence-schema, runtime, worker, CI, and secrets changes need explicit rollback/compatibility review.
  • Run the narrowest meaningful checks, plus cargo test -p ironclaw_architecture_tests when dependency edges, layer keys, crate placement, or test-pinned guidance files change.
  • Search changed production files for .unwrap()/.expect(), suspicious byte slicing, hardcoded temporary paths, and lost error causes.
  • When a trait changes, enumerate all implementations, decorators, adapters, and test doubles; when a pattern bug is fixed, search crates/ for sibling instances.
  • After moves/renames, search agent guidance, contracts, docs, tests, scripts, manifests, and frontend imports for old paths.
  • Update the owning contract/docs when behavior changes; the PR title/body must describe every layer in the diff and note compatibility, rollback, and follow-up risks.