* 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>
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_assistantowns product-facing orchestration andProductSurface; 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 areironclaw_skills; persistent memory isironclaw_memory(model toolsironclaw.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 usethiserrorfor error types inerror.rs. Cause-preserving constructors, themap_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_commonis not a dumping ground. - No
pub usere-exports unless exposing to downstream consumers. - Prompt templates live in files, not Rust code: multi-line prompt strings go in a
prompts/*.mdfile inside the crate that owns the behavior, loaded viainclude_str!()(ls -d crates/*/*/prompts crates/extensions/packages/*/promptslists 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
- 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.
- Consolidate, don't proliferate. Extend the test that already exercises the path; add a new test only for a genuinely distinct scenario.
- Integration-first. Production-wired behavior ships with a test in
tests/integration/, driven through the harness and asserting at a seam — neverwait_for_status(Completed)alone. Crate tier is the fallback only when that tier cannot reach the path (say why in the PR). - 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
ProductSurfaceand 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 / auth — ironclaw_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_testswhen 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.