For AI coding agents working on Adjutant. Not read at runtime.
- NEVER read KB files directly. Always query via CLI:
.venv/bin/python -m adjutant kb query <name> "<question>". No Read, Glob, Grep,cat, orlson any KB path.
Python-based persistent agent framework. An LLM agent receives messages via Telegram, queries sandboxed KB sub-agents, and orchestrates lifecycle/heartbeat logic. CLI entrypoint: adjutant (bash shim → python -m adjutant).
adjutant/ # Monorepo
├── adjutant # CLI shim
├── adjutant.yaml.example # Config template (adjutant.yaml gitignored)
├── .env.example # Secrets template (.env gitignored)
├── .opencode/agents/adjutant.md # Main agent definition (tracked)
├── identity/ # Soul/heart/registry (gitignored)
├── knowledge_bases/ # registry.yaml (gitignored)
├── templates/kb/ # KB scaffold templates
├── prompts/ # pulse.md, review.md, escalation.md
├── src/adjutant/ # Python framework
│ ├── cli.py # Click CLI
│ ├── __main__.py
│ ├── core/ # backend, backend_opencode, backend_claude_cli, config, env, lockfiles, logging, model, opencode, paths, platform, process
│ ├── lib/ # http, ndjson, claude_json
│ ├── lifecycle/ # control, cron, update
│ ├── observability/ # status, usage_estimate, journal_rotate
│ ├── capabilities/
│ │ ├── kb/ # manage, query, run
│ │ ├── schedule/ # install, manage, notify_wrap
│ │ ├── screenshot/ # screenshot.py + playwright_screenshot.mjs
│ │ ├── search/ # search.py
│ │ └── vision/ # vision.py
│ ├── news/ # fetch → analyze → briefing pipeline
│ ├── setup/ # install, repair, uninstall, wizard + steps/
│ └── messaging/
│ ├── adaptor.py, dispatch.py
│ └── telegram/ # chat, commands, listener, notify, photos, send, service
├── tests/unit/ # ~56 files, ~1139 tests
├── tests/integration/ # lifecycle, feature gating, plist tests
│
├── web/ # Adjutant Web (KB explorer dashboard)
│ ├── package.json # npm workspace root
│ ├── api/ # Express REST API (port 3020)
│ │ ├── src/
│ │ │ ├── index.ts # Express app setup, createApp(), server start
│ │ │ ├── config.ts # Static config (port, host, ~/.adjutant-web paths)
│ │ │ ├── routes/ # config, kbs, folders, notes, assets, adjutant
│ │ │ ├── services/ # configService, kbService, folderService, fileNoteService, imageService, registryService
│ │ │ ├── middleware/ # auth (session token), accessControl (read-only KBs)
│ │ │ └── types/ # config, kb, folder (WebSidecar), note
│ │ ├── vitest.config.ts
│ │ └── package.json
│ └── app/ # React 19 frontend (port 3021)
│ ├── src/
│ │ ├── App.tsx # Main app: routing, state orchestration
│ │ ├── types/ # Shared TypeScript types
│ │ ├── hooks/ # useKbs, useFolder, useNotes, useImages, useSettings, useAdjutant, useCanvas*
│ │ ├── contexts/ # EditorContext, PlacementContext
│ │ └── components/ # Canvas, Home, Sidebar, NoteEditor, nodes/, Toolbar, etc.
│ ├── index.html
│ ├── vite.config.ts
│ └── package.json
│
├── site/ # Docusaurus documentation site
│ ├── (reads ../docs via config) # path: '../docs' in docusaurus.config.ts
│ ├── docusaurus.config.ts
│ ├── sidebars.ts
│ └── package.json
│
├── integrations/
│ └── openwebui/ # Open WebUI filter/pipe integration
│ ├── adjutant_web_filter.py
│ ├── adjutant_web_pipe.py
│ └── README.md
│
├── docs/ # Source-of-truth documentation
│ ├── architecture/
│ ├── development/
│ ├── guides/
│ ├── plans/
│ ├── reference/
│ └── web/ # Web dashboard docs
└── pyproject.toml # Python build (hatchling)
Gitignored: identity/, state/, journal/, insights/, photos/, screenshots/, .env, adjutant.yaml, knowledge_bases/registry.yaml, web/*/node_modules/, site/node_modules/, site/build/.
- All source under
src/adjutant/— no top-level modules - Imports: stdlib → third-party → local, alphabetical within groups
- Credentials:
get_credential(key)fromcore/env.py— never read.envdirectly - Paths:
get_adj_dir()fromcore/paths.py— never hardcode~/.adjutant - Logging:
adj_log("component", "message")— notprint() - Capability functions return a result string or raise — no stdout
- Temp files:
NamedTemporaryFile(delete=False)+finally: os.unlink(tmp) - New modules need
tests/unit/test_<module>.py
cmd_*— slash command handlers (messaging/telegram/commands.py)msg_*— messaging interface (adaptor.py+ telegram/)kb_*— KB CRUD (capabilities/kb/manage.py)wiz_*/step_*— wizard UI / setup steps_*— private/internal
-
Create
src/adjutant/capabilities/<name>/<name>.py— return string or raise -
Add
cmd_<name>()inmessaging/telegram/commands.py -
Register in
messaging/dispatch.py -
Add CLI command in
cli.py -
Document in
.opencode/agents/adjutant.md -
Add
tests/unit/test_<name>.py -
Add to
docs/guides/commands.md -
Use
backend.run()for LLM calls — never import backend implementations directly
Full guide: docs/development/plugin-guide.md
Adjutant supports two LLM backends: OpenCode (opencode) and Claude Code CLI (claude-cli). The active backend is set in adjutant.yaml under llm.backend.
All LLM calls go through the backend abstraction. Import get_backend from core/backend.py:
from adjutant.core.backend import get_backend
backend = get_backend()
result = await backend.run(prompt, agent="adjutant", model=model)Never import backend_opencode or backend_claude_cli directly. Never call opencode_run() from call sites.
Web server services: The native opencode web --mdns and cloudcli --port web servers used to be spawned by lifecycle/control.py for remote access. They have been retired — adjutant's own web/app (served from web/api) is the remote UI now, and the Python daemon no longer manages any backend-side web server. BackendCapabilities.web_server is False on both backends for the same reason.
Check capabilities before optional features:
if backend.capabilities.vision:
result = await backend.run(prompt, files=[image_path])
if backend.capabilities.model_listing:
models = await backend.list_models()Error handling: Check result.error_type against the shared taxonomy (model_not_found, auth_failure, rate_limited, context_overflow, permission_denied, vision_unsupported, timeout, parse_error, error).
Full guide: docs/development/backend-guide.md
Register in dispatch.py (exact match + prefix match). Handler signature:
async def cmd_mycommand(arg: str, message_id: int, adj_dir: Path, *, bot_token: str, chat_id: str) -> None:For long-running commands, use msg_typing_start()/msg_typing_stop() and run in a background task.
KBs are sandboxed workspaces. The main agent never reads KB files — it queries via sub-agent.
result = await kb_query("mybase", "What is the status?", adj_dir)- Scaffold generated from
templates/kb/bykb_scaffold()inmanage.py - Registry at
knowledge_bases/registry.yaml— pure-Python YAML parsing (nopyyaml) read-onlyKBs deny bash/edit/write;read-writeKBs only deny external_directory
Full guide: docs/guides/knowledge-bases.md
The documentation site lives at site/ (Docusaurus, deployed to GitHub Pages). Its docs/ directory is a symlink to the repo's docs/ — single source of truth, no manual mirroring.
- Edit the file in
docs/(source of truth) - The
site/build picks it up automatically via symlink - To test the site locally:
cd site && npm start - To build:
cd site && npm run build
- New or modified CLI commands →
docs/guides/commands.md - New capabilities →
docs/guides/(dedicated guide) +docs/development/plugin-guide.md - Config changes →
docs/guides/configuration.md - Architecture changes →
docs/architecture/ - New slash commands →
docs/guides/commands.md - Web dashboard changes →
docs/web/
.opencode/agents/adjutant.md is tracked — edit when capabilities, routing, or tool patterns change.
identity/soul.md, heart.md, registry.md are gitignored — never edit programmatically.
Adjutant Web is the local-first KB explorer dashboard. It discovers Adjutant-format KBs and presents them on an infinite spatial canvas.
- API (
web/api/): Express 4.x, TypeScript ~5.3, Zod, Sharp, Vitest - Frontend (
web/app/): React 19, React Flow (@xyflow/react), TipTap, Vite 7.x, CSS Modules
cd web && npm install # Install both api + app deps (npm workspaces)
cd web/api && npm run dev # API on :3020 (hot reload)
cd web/app && npm run dev # Vite on :3021 (hot reload)
cd web/api && npm test # API tests (~87 tests)
cd web/app && npx tsc -b --noEmit # TypeScript check.adjutant-web.json— sidecar files in each KB folder storing canvas positions, sections, stickies<kb>/.adjutant-web/assets/— uploaded images (WebP + thumbnails)~/.adjutant-web/config.json— app config (kbRoot path)- Notes are pure
.mdfiles — no frontmatter
WebSidecar/WebSidecarSchema— the.adjutant-web.jsonroot schema (items, sections, stickies, images)NoteFile/NoteMeta— note data structuresKbMeta— KB metadata fromkb.yaml
| Variable | Default | Description |
|---|---|---|
ADJUTANT_WEB_PORT |
3020 |
API server port |
ADJUTANT_WEB_HOST |
0.0.0.0 |
API server host |
ADJUTANT_WEB_SESSION_TOKEN |
(none) | Session token for auth (set by Adjutant on startup) |
ADJUTANT_DIR |
(auto-detected) | Adjutant root directory. In monorepo, defaults to repo root. |
| Type | Component | Description |
|---|---|---|
note |
NoteNode |
Note card with title + TipTap preview. Double-click opens editor. |
image |
ImageNode |
Image with upload/error/ready states. Corner resize. |
section |
SectionNode |
Grouping rectangle with editable label. Resize handles. |
sticky |
StickyNode |
Colored sticky with inline text editing. 9 color variants. |
- Strict mode — avoid
anytypes .jsextension for all local imports in the API (ESM requirement)import typefor type-only imports- CSS Modules for all component styling
- Custom hooks for data fetching — components stay presentational
- Optimistic updates with debounced saves (300ms)
- Barrel exports (
index.ts) in each component directory
The web dashboard reads Adjutant's state via registryService.ts (resolves ADJUTANT_DIR → reads knowledge_bases/registry.yaml). The /api/adjutant/* routes provide lifecycle control, health checks, schedule management, and journal access — all via Adjutant's CLI or filesystem state.
The source field in active_operation.json uses "adjutant-web" when operations are triggered from the dashboard.
# Python tests
.venv/bin/pytest tests/ -q # full suite (~80s, ~1160 tests)
.venv/bin/pytest tests/unit/ -q # unit tests only (~75s, ~1139 tests)
.venv/bin/pytest tests/integration/ -q # integration tests only (~5s)
.venv/bin/pytest tests/unit/test_kb_manage.py -q # single file
# Web dashboard tests
cd web/api && npm test # API tests (~87 tests)
cd web/app && npx tsc -b --noEmit # TypeScript type check
# Documentation site
cd site && npm run build # Docusaurus buildAll tests must pass before release. No CI. Full guide: docs/development/testing.md
- Backend abstraction: Never call
opencode_run()from call sites — always useget_backend().run(). The old function still exists incore/opencode.pybut is only used internally bybackend_opencode.py. - Claude CLI
--dangerously-skip-permissions: Required for non-interactive subprocess mode. Bypasses.claude/settings.jsondeny rules — hooks in.claude/hooks/are the primary defense. - Backend capabilities: Always check
backend.capabilities.*before calling optional methods likelist_models(),reap(), or passingfiles=for vision. Not all backends support all features. - Model ID formats differ: OpenCode uses
anthropic/claude-sonnet-4-6, Claude CLI usessonnet. The backend'sresolve_alias()handles this transparently, but raw model IDs in state files may need translation during backend switches. kb_list()returnsKBEntryobjects, not dicts — use.name,.description,.accesskb_info()/kb_remove()raiseValueError, notKBNotFoundErrorkb_quick_create()takeskb_pathasstr, notPathschedule_get()returnsNonewhen not found — does not raiseresolve_command()is public inschedule/manage.py(was_resolve_commandbefore v0.2.0)dispatch_photoarg order differs fromdispatch_message— check signatureNDJSONResultvsOpenCodeResult— don't mix at call sitesdispatch.pyauth + rate-limit + feature-gate block is security-critical — run full tests before refactoring- Feature-gated commands (
/screenshot,/search) are rejected at dispatch if disabled in config — add new gates to_FEATURE_GATESindispatch.py - Cron line length: macOS cron silently skips lines over ~1,024 chars.
_snapshot_path()inschedule/install.pymust build a minimal PATH — never dump the full$PATHfrom the shell. Afterschedule sync, verify withcrontab -l | wc -cper line.