Codebase Organization
The codebase has two trees: src/ (the runtime kernel) and skills/ (the tool library). A strict one-way dependency governs them: skills import from src, never the reverse. Tests mirror both trees under test/.
src/ — The Runtime Kernel
Eight concern-based subdirectories, plus root-level files:
| Path | Responsibility |
|---|---|
src/types.ts |
Single source of truth for all types and interfaces. No type definitions elsewhere. |
src/index.ts |
Public API re-exports. |
src/cli.ts |
CLI entry point. |
src/core/ |
Pure utilities with no framework knowledge: path expressions, serialization, topo-sort, ring-buffer, fuzzy matching, math, terminal detection, readline, state-store. |
src/cascade/ |
The cascade algorithm: orchestrator, merge strategies, annotations, inheritance, string interpolation. |
src/context/ |
Context creation, ContextManager, helpers (ctxTarget, ctxCwd, ctxPaths), manager-dispatch. |
src/tools/ |
Tool resolution (lookup.ts), loading (loader.ts), registry (registry.ts), validation, allowed-tools parsing, module import. |
src/log/ |
Logging system (log.ts, types.ts) and reporters/ subdirectory. |
src/boot/ |
Bootstrap (the one builtin) and MCP server initialization. |
src/cli/ |
CLI argument parsing. |
src/testing/ |
Test helpers (mockContext, mockMiddlewareContext). |
src/core/ Rule
src/core/ contains pure utilities that have no knowledge of the framework's domain concepts (contexts, tools, cascades). A module belongs in src/core/ only if it is used by two or more consumers. Single-consumer utilities are absorbed into their consumer — this was established during the codebase reorganization when scc.ts was absorbed into cascade/orchestrator.ts and glob.ts into tools/lookup.ts.
npm Dependencies in src/
The runtime kernel uses a small set of npm packages, each confined to specific modules:
| Package | Used in | Purpose |
|---|---|---|
gray-matter |
tools/loader.ts, tools/lookup.ts |
YAML frontmatter parsing |
ajv |
tools/validate.ts |
JSON Schema validation |
picomatch |
tools/lookup.ts |
Glob pattern matching |
jsonpath-plus |
core/path.ts |
JSONPath expression evaluation |
js-yaml |
log/reporters/format.ts |
YAML rendering for log output |
Everything else in src/ uses only Node.js builtins.
File Size Aspirations
src/ files target ~200 lines. skills/ core tools target ~60 lines. skills/extensions/ tools have no specific target — they are larger by nature. These are aspirational, not rigid. Files can exceed them when the problem demands it.
skills/ — The Tool Library
Skills are organized into concern-based directories. The top-level directories under skills/ are not a fixed set — new tiers can be added as the framework grows. Each directory groups skills by their role in the system.
Core Tiers
| Directory | Role | npm deps |
|---|---|---|
skills/pipeline/ |
Standard pipeline middleware — execute, validate-args, directives, cwd, paths, params, model, etc. Includes default.skill.md (the library main skill). |
No |
skills/directives/ |
Directive implementations — arg, env, path, skill, eval, inline, script, etc. Plus shared directive utilities (directive-utils.ts, directive-types.ts). |
No |
skills/runtime/ |
Core runtime skills — events, callbacks, context-manager, invoke, cache-clear, and meta skills (skill-list, skill-describe, skill-register). | No |
skills/tools/ |
User-facing utility tools — files (read, write, list, glob, search), shell, code, help, web-fetch. | No |
skills/context/ |
Context skills (role: context) — prompt content injected into agent tool documentation. Not invoked directly. |
No |
Core tiers do not use npm dependencies. They import only from src/ and from sibling files within their own directory.
Extensions
skills/extensions/ contains larger, more opinionated features that may use npm packages and have complex internal structure:
| Subdirectory | Feature | Key npm deps |
|---|---|---|
agent/ |
Agent policy, Strands/Bedrock provider, Kiro provider, proxy, sandbox | @strands-agents/sdk, isolated-vm |
web/ |
HTTP server and route dispatch | path-to-regexp |
websocket/ |
WebSocket server and socket dispatch | — |
email/ |
SES/S3 email polling and dispatch | — |
slack/ |
Socket Mode Slack integration | — |
webhook/ |
Standard Webhooks receiver | — |
gateway/ |
Channel send/receive dispatch (CLI, Slack, email, web) | — |
hub/ |
Package registry (search, add, remove, update, publish) | — |
toolchain/ |
Compiler suite (compile, decompile, test, analyze, revise, architect, etc.) | — |
trust/ |
Tool execution gating with approval prompts | — |
roles/ |
Identity-based metadata injection | — |
session/ |
Conversational memory across invocations | — |
schedule/ |
Timer-based skill scheduling with cron, file persistence | croner |
state/ |
Key-value persistent state | — |
diagnostics/ |
Debug, profile, observe hooks | — |
mcp/ |
MCP server (expose skills as MCP tools) | zod, @modelcontextprotocol/sdk |
infra/ |
Deploy (App Runner) and sandbox (Docker) | — |
auth/ |
Auth providers (token, OAuth, internal) | jose |
amazon/ |
Amazon-internal auth and deploy providers | jose |
repl/ |
Interactive agent session | — |
tutor/ |
Interactive learning lessons | — |
init/ |
Project scaffolding | — |
update/ |
CLI update | — |
event/ |
Event metadata middleware | — |
Each extension subsystem owns its own helper files (named *-utils.ts, *-types.ts, *-state.ts, *-lib.ts, *-store.ts). These are private to their directory.
Import Hierarchy
The dependency graph is strictly layered:
src/core/ Pure utilities, no framework knowledge
↑
src/{cascade,context,tools, Runtime subsystems (import freely from
log,boot,cli,testing} each other and from core/)
↑
skills/{pipeline,directives, Core skills (import from src/ only,
runtime,tools,context} plus sibling files in own directory)
↑
skills/extensions/ Extensions (import from src/ and
downward into core skill tiers)
Arrows mean "may import from." No imports flow in the reverse direction. src/ never imports from skills/. Core skills never import from extensions.
What Skills Import from src/
The most common imports from src/ into skills, by frequency:
| Module | Purpose | Typical importers |
|---|---|---|
src/types.ts |
Context, JsonObject, ToolDef, etc. |
Every skill |
src/context/helpers.ts |
ctxTarget, ctxCwd, ctxPaths, toText |
All middleware |
src/tools/lookup.ts |
findTool, listTools, registerTool, createCallbackRef, matchPattern |
Runtime skills, extensions |
src/core/serialize.ts |
stringify, parse |
Skills that format output |
Other src/ modules are imported sparingly and only by skills that genuinely need them.
Cross-Tier Imports Within skills/
Most skills only import from src/ and from sibling files in their own directory. The following cross-tier imports exist within the library and are considered sanctioned:
| From | To | What | Why |
|---|---|---|---|
skills/pipeline/ |
skills/directives/ |
extractDirectives, stripHtmlComments, toolDir |
Pipeline orchestrates directive processing |
skills/extensions/* |
skills/extensions/gateway/gateway-utils.ts |
setGateway |
Shared gateway interface for all transport providers |
skills/extensions/* |
skills/runtime/events/event-types.ts |
Event type |
Type-only import for event handling |
skills/extensions/event/ |
skills/runtime/events/event-state.ts |
getEvents |
Event middleware checks subscription state |
skills/extensions/session/ |
skills/directives/ |
DirectiveArgs type, escapeRegex |
Session-history is a directive |
skills/extensions/toolchain/ |
skills/directives/ |
extractDirectives |
Toolchain analyzes directive usage in skills |
All cross-tier imports flow downward (extensions → core tiers) or laterally within the same concern. No core skill imports from extensions.
Rule for External Skills
Hub packages and user-authored skills should import only from src/ (via the agent-apps package entry point) and from their own files. Cross-skill imports are an internal library convenience — external skills must not depend on the library's internal file structure.
Utility File Convention
Non-.skill.ts TypeScript files within skills/ are private helpers scoped to their containing directory. There is no shared utility directory in skills/. This was established when skills/common/ (a 12-file grab bag of shared utilities) was eliminated during the codebase reorganization — its contents were distributed to src/ (for pure utilities) or to their owning skill subsystem (for domain-specific helpers).
Naming conventions for helper files:
| Suffix | Purpose | Examples |
|---|---|---|
*-utils.ts |
Shared utility functions | directive-utils.ts, gateway-utils.ts, email-util.ts |
*-types.ts |
Type definitions | directive-types.ts, agent-types.ts, infra-types.ts |
*-state.ts |
Globals-backed state accessors | event-state.ts, callback-state.ts, slack-state.ts |
*-lib.ts |
Substantial shared logic | hub-lib.ts, trust-lib.ts, state-lib.ts |
*-store.ts |
Persistence helpers | trust-store-lib.ts, schedule-store.ts, token-store.ts |
Test Organization
Tests mirror the source tree:
| Source | Test |
|---|---|
src/cascade/merge.ts |
test/src/cascade/merge.test.ts |
skills/pipeline/cwd.skill.ts |
test/skills/pipeline/cwd.test.ts |
skills/extensions/agent/agent.skill.ts |
test/skills/extensions/agent/agent.test.ts |
Integration tests (test/integration/) are organized by subsystem. Acceptance tests (test/acceptance/) are organized by feature. See Testing Strategy for the full test methodology.
Enforcement
- madge runs on every commit via lefthook, checking for circular dependencies across
src/andskills/combined. Onlysrc/types.tsandsrc/cascade/inherit.tsare excluded (they have legitimate circular references with their consumers). - Convention — consistent patterns across hundreds of files, enforced by code review.
npm run check— TypeScript strict mode, Biome linting, knip (unused exports), eslint (type-aware rules), and madge all run together.
Skill Design Conventions
These conventions govern how skills expose configuration, accept arguments, and interact with each other. They ensure consistency across the library so skills feel cohesive rather than ad-hoc.
One Way to Write Things
Every configuration shape has exactly one canonical form. No shorthand aliases, no "if this key is missing, treat the whole object as a default." If a skill accepts a list, it always takes a list — even for the single-item case. This eliminates ambiguity in documentation, tests, and user mental models.
Wrong: Two ways to write the same config.
# "Shorthand" — bare object treated as default
email:
from: bot@example.com
bucket: ses-bucket
# "Explicit" — wrapped in profiles
email:
profiles:
default:
from: bot@example.com
bucket: ses-bucket
Right: One canonical form.
email:
from: bot@example.com
bucket: ses-bucket
routes:
- ref: handler-skill
Lists Over Keyed Maps for Iteration
When a skill iterates over a collection (routes, providers, rules), use an array of objects — not a keyed map. Keyed maps are for lookup by name (e.g., deploy-app --arg name=prod). If the key is only used for logging, it doesn't justify a map.
Wrong: Keyed map where the key is cosmetic.
slack:
profiles:
my-bot:
app-token: xapp-...
routes:
- ref: handler
Right: Flat config with a routes list.
slack:
app-token: xapp-...
routes:
- ref: handler
Keyed maps are appropriate when the key is a lookup target:
deploy:
profiles:
prod:
provider: apprunner
region: us-east-1
Consistent Key Names
Skills that dispatch inbound messages to other skills use the same vocabulary:
| Key | Meaning | Used by |
|---|---|---|
routes |
List of dispatch rules, each with a ref to the target skill |
web, slack, email, webhook, websocket |
ref |
Skill name to invoke when a route matches | All route entries |
verify |
List of authentication providers tried in order | auth |
rules |
List of pattern-matching rules | roles |
Filter properties on route entries are transport-specific (from, subject, method, path, socket, etc.) but ref is universal.
Optional name Field
Every list entry (routes, verify, rules) supports an optional name property. It serves two purposes:
- Logging and correlation. The name appears in log events and gateway identity.
- Targeted lookup. Consumer skills that need to reference a specific entry search by
name(e.g.,email-setup --name inbox,auth-token-create --name api).
When name is omitted, a sensible default is derived from the entry's required fields:
| List | Default name |
|---|---|
| slack routes | ref value |
| email routes | ref value |
| webhook routes | path value |
| websocket routes | socket value (required) |
| auth verify | provider value |
| roles rules | match pattern |
Do not require name when a sensible default exists. Users only set it when they need to target a specific entry from another skill.
Gateway Integration
Transport middleware (web, slack, email, websocket, webhook, MCP) sets up a gateway for each inbound connection using setGateway() from gateway-utils.ts. The gateway's send and receive callbacks are real functions that call the provider skills with correlation args baked in. The callback ref params reflect the transport-specific args, not a generic union.
Skills that need to communicate with the user call gateway-send and gateway-receive — they never call provider skills directly unless reaching a different user or transport.
Middleware Metadata Params
Middleware skills declare params: { type: "object" } as a minimum. The params declaration documents what the middleware accepts as cascade metadata — it is informational, not enforced by validation. Middleware params should enumerate their properties so agents and MCP callers can discover the configuration shape.
Skill Signatures
All skills use typed signatures:
| Pattern | When |
|---|---|
(ctx: Context) |
No args expected |
(ctx: Context, args: DirectiveArgs) |
Directive skills |
(ctx: Context, args: unknown) |
Skills that narrow args internally |
(ctx: Context, args: { specific: types }) |
Skills with known arg shapes |
Middleware skills always call ctxTarget(ctx) to get the served context. They never operate on ctx directly (which is the middleware's own context).
Frontmatter Conventions
name: lowercase-with-hyphens, max 64 characters, no double hyphensdescription: one sentence, present tense, describes what the skill doesmetadata.role:main,middleware, orcontextmetadata.visibility:hiddenfor internal plumbing skillsmetadata.tags: lowercase-with-hyphens,meta-internalfor framework internals,meta-metadatafor middleware that processes cascade keysmetadata.$private: ["$self"]: prevents the skill from appearing as a toolKey in its own cascade (required for middleware that appears indefault.skill.md) and keeps per-skill middleware private automatically (transport middleware, session, state, auth, roles, schedule, event, delegate)metadata.$global: ["$self"]: auto-promotes the skill's cascade value to all tools when set on the main skillallowed-tools: top-level frontmatter field (not under metadata), space-delimited glob patterns