Compare commits
66
Commits
v1.16.1
...
271721694b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
271721694b | ||
|
|
171597ca05 | ||
|
|
7b0b53671f | ||
|
|
884b19ccb5 | ||
|
|
6db00b2266 | ||
|
|
7f64423615 | ||
|
|
6d3f4918bf | ||
|
|
448eb5d462 | ||
|
|
924576d2ee | ||
|
|
033f964be0 | ||
|
|
2de5b57133 | ||
|
|
d1929f2fd4 | ||
|
|
14aa1785f8 | ||
|
|
0e0fd5df05 | ||
|
|
07f547eea8 | ||
|
|
f918d44c8d | ||
|
|
0d06875bb7 | ||
|
|
ffefa8c07f | ||
|
|
883908ab55 | ||
|
|
eed4025918 | ||
|
|
6b90c7eb0d | ||
|
|
958645ed7f | ||
|
|
eb8cc17993 | ||
|
|
b3a4d131d1 | ||
|
|
f368f3a1c0 | ||
|
|
8ecc588a3e | ||
|
|
55677dd671 | ||
|
|
d615090dcd | ||
|
|
8c58faf292 | ||
|
|
802346f909 | ||
|
|
552bc5bc63 | ||
|
|
d59713d3e3 | ||
|
|
6ab532018a | ||
|
|
047d7183d7 | ||
|
|
4d85e5144b | ||
|
|
dcc8c3ee42 | ||
|
|
289c58ef36 | ||
|
|
094eb4b8ba | ||
|
|
dd7825b481 | ||
|
|
4c186b62d4 | ||
|
|
fef3c925cd | ||
|
|
785ae19757 | ||
|
|
873f870e23 | ||
|
|
54927f692f | ||
|
|
efcd191f96 | ||
|
|
2e8a4f2443 | ||
|
|
2f5f62ab09 | ||
|
|
919435eb84 | ||
|
|
bed9f8cff6 | ||
|
|
6c7995f5f5 | ||
|
|
7ea226509c | ||
|
|
6bef4a3f82 | ||
|
|
16494d4b1e | ||
|
|
f84dfb8476 | ||
|
|
07b217cf4a | ||
|
|
9bfb95d795 | ||
|
|
285dbb14cc | ||
|
|
cac6626586 | ||
|
|
5a373d1031 | ||
|
|
fe2e916937 | ||
|
|
66ac4dbf02 | ||
|
|
44c3dd1239 | ||
|
|
87abe8c335 | ||
|
|
148ba4e07b | ||
|
|
89ee213454 | ||
|
|
a04651905f |
No files matched your search
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: commit-convention
|
||||
description: Conventional Commits format and version-bump rules for this repo (Bahasa Indonesia commit style). Use when creating a git commit in zesdex.
|
||||
---
|
||||
|
||||
# Commit Convention
|
||||
|
||||
Gunakan **Conventional Commits** untuk semua commit. Format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
**Type & efek ke versi:**
|
||||
|
||||
| Type | Bump | Kapan pakai |
|
||||
|-------------|-------|------------------------------------------|
|
||||
| `feat` | minor | Fitur baru |
|
||||
| `fix` | patch | Perbaikan bug |
|
||||
| `chore` | patch | Maintenance, update deps, dll |
|
||||
| `docs` | patch | Perubahan dokumentasi/comment |
|
||||
| `refactor` | patch | Refactor kode tanpa perubahan fungsional |
|
||||
| `test` | patch | Nambah/ubah test |
|
||||
| `style` | patch | Formatting, whitespace, lint |
|
||||
| `perf` | patch | Optimasi performa |
|
||||
| `ci` | patch | Perubahan CI/CD |
|
||||
|
||||
**Catatan:**
|
||||
- **Semua type menghasilkan release** (patch minimal). Tidak ada commit yang "skip release".
|
||||
- Tambahkan `BREAKING CHANGE:` di body commit untuk bump **major**.
|
||||
- **Scope** opsional, tapi direkomendasikan (misal `feat(agent):`, `fix(ipc):`).
|
||||
|
||||
### Contoh
|
||||
|
||||
```
|
||||
feat(tool): add batch file delete
|
||||
|
||||
chore: bump reqwest to 0.12
|
||||
|
||||
refactor(harness): flatten guard pipeline
|
||||
|
||||
fix(ipc): reconnect loop on socket timeout
|
||||
|
||||
docs: add architecture diagram to README
|
||||
|
||||
BREAKING CHANGE: IPC frame header changed from 4-byte to 8-byte length
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Rust build artifacts
|
||||
target/
|
||||
|
||||
# VCS
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Local/secret files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Editor
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# Nix
|
||||
result
|
||||
result-*
|
||||
|
||||
# Kilo metadata
|
||||
.kilo/
|
||||
|
||||
# Docs lessons
|
||||
docs/lesson/
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Build workspace
|
||||
run: cargo build --release --workspace
|
||||
|
||||
- name: Test workspace
|
||||
run: cargo test --workspace
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Clippy workspace
|
||||
run: cargo clippy --workspace -- -D warnings
|
||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Test workspace
|
||||
run: cargo test --workspace
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Build & Deploy (Nix)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.VPS_USER }}
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
with:
|
||||
determinate: false
|
||||
extra-conf: |
|
||||
sandbox = false
|
||||
accept-flake-config = true
|
||||
|
||||
- name: Cache Nix
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v14
|
||||
|
||||
- name: Build zesdex
|
||||
id: build
|
||||
run: |
|
||||
nix build .#default --impure --option sandbox false --print-build-logs
|
||||
STORE_PATH=$(readlink result)
|
||||
echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT"
|
||||
echo "Build OK: $STORE_PATH"
|
||||
|
||||
- name: Setup SSH key
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null 2>&1 || { echo "SSH key invalid"; exit 1; }
|
||||
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Deploy zesdex to VPS
|
||||
run: |
|
||||
STORE_PATH="${{ steps.build.outputs.store-path }}"
|
||||
ssh "$VPS_USER@$VPS_HOST" "nix copy --to file:///nix/store $STORE_PATH && nix-env --install --force $STORE_PATH --profile /nix/var/nix/profiles/zesdex && systemctl restart zesdex"
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Publish to FlakeHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
flakehub-publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: DeterminateSystems/determinate-nix-action@main
|
||||
- uses: DeterminateSystems/flakehub-push@main
|
||||
with:
|
||||
visibility: public
|
||||
rolling: true
|
||||
+2
-1
@@ -5,4 +5,5 @@ node_modules/
|
||||
package.json
|
||||
package-lock.json
|
||||
.superpowers/
|
||||
docs/lesson/
|
||||
docs/lesson/
|
||||
.kilo/
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"branches": ["main"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
["@semantic-release/exec", {
|
||||
"prepareCmd": "sed -i 's/^version = \"[^\"]*\"/version = \"${nextRelease.version}\"/' Cargo.toml && cargo check"
|
||||
}],
|
||||
["@semantic-release/git", {
|
||||
"assets": ["Cargo.toml", "CHANGELOG.md"],
|
||||
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}],
|
||||
["@semantic-release/github", {
|
||||
"assets": []
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
Codemap Update Report — 2026-07-12
|
||||
====================================
|
||||
|
||||
Status: FIRST GENERATION (no previous codemaps to compare)
|
||||
|
||||
Files created:
|
||||
- docs/CODEMAPS/architecture.md (new)
|
||||
- docs/CODEMAPS/backend.md (new)
|
||||
- docs/CODEMAPS/frontend.md (new)
|
||||
- docs/CODEMAPS/data.md (new)
|
||||
- docs/CODEMAPS/dependencies.md (new)
|
||||
|
||||
Source scanned:
|
||||
- 124 Rust source files
|
||||
- 30 directories
|
||||
- 103 modules
|
||||
- 10,402 lines total
|
||||
|
||||
No previous codemaps found — diff calculation skipped.
|
||||
Freshness: all documents generated 2026-07-12.
|
||||
@@ -0,0 +1,38 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Kilo when working with code in the zesdex repository.
|
||||
|
||||
## Best Practice Conventions
|
||||
|
||||
Zesdex follows Kana Engineering Best Practices:
|
||||
|
||||
1. **Clean Architecture** — Strict domain/application/infrastructure/presentation layering.
|
||||
- Domain has ZERO framework dependencies.
|
||||
- Application depends only on domain.
|
||||
- Infrastructure implements domain traits.
|
||||
- DTOs cross layer boundaries, NOT entities.
|
||||
|
||||
2. **Clean Code** — Functions under ~40 lines, one level of abstraction per function, descriptive names, no flag arguments, no commented-out code.
|
||||
|
||||
3. **Documentation** — Every pub fn, struct, enum, and trait needs a doc comment (///) explaining what, flow, why, and return value.
|
||||
|
||||
4. **Commit Convention** — Conventional Commits in Bahasa Indonesia: `feat(scope):`, `fix(scope):`, `chore:`, `docs:`.
|
||||
|
||||
5. **Error Handling** — `anyhow::Result` and `anyhow::bail!` throughout. Log with `tracing` (never stderr).
|
||||
|
||||
6. **Testing** — `#[cfg(test)] mod tests` blocks inline in production files. Tests are F.I.R.S.T. (Fast, Independent, Repeatable, Self-validating, Timely).
|
||||
|
||||
7. **No Compiler Bypasses** — Never use `#[allow(...)]`, `#[expect(...)]`, or `#[allow(dead_code)]`. Fix the underlying code.
|
||||
|
||||
8. **Boy Scout Rule** — Leave every module cleaner than you found it.
|
||||
|
||||
## Available Agents
|
||||
|
||||
- `@rust-engineer` — Rust clean architecture specialist (subagent).
|
||||
- `@code-reviewer` — Code review specialist (subagent).
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `/check` — Run cargo check, clippy, and tests.
|
||||
- `/audit` — Code quality audit against clean-architecture best practices.
|
||||
- `/doc` — Generate or update doc comments.
|
||||
@@ -1,3 +1,77 @@
|
||||
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** cegah race condition pada register users.json (TOCTOU) ([884b19c](https://github.com/asepharyana/zesdex/commit/884b19ccb5fbcaa6b29cb41dc978386cf7b1b3f9))
|
||||
* **api:** perbaiki keamanan auth & WebSocket, tambah rate limiting ([6db00b2](https://github.com/asepharyana/zesdex/commit/6db00b22663839a6a975ae978058b894900c71de))
|
||||
* **build:** perbaiki referensi paket zesdex-gateway dan sinkronisasi versi nix ([6d3f491](https://github.com/asepharyana/zesdex/commit/6d3f4918bfea06b106d4fe75adf58e6a29aca2a5))
|
||||
* **build:** perbaiki referensi paket zesdex-gateway di Dockerfile & default.nix ([7f64423](https://github.com/asepharyana/zesdex/commit/7f644236158f66fcc106ec55e437ebf28ee1cca1))
|
||||
|
||||
## [1.18.2](https://github.com/asepharyana/zesdex/compare/v1.18.1...v1.18.2) (2026-08-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nix:** add perl to nativeBuildInputs for openssl-sys Configure ([033f964](https://github.com/asepharyana/zesdex/commit/033f964be0c695106eef74329524ad7b4b8dba14))
|
||||
* **nix:** correct cargoBuildFlags package name zesdex-backend -> zesdex-gateway ([2de5b57](https://github.com/asepharyana/zesdex/commit/2de5b57133f6c7295d8a85e63cb86504ea2a7d3e))
|
||||
|
||||
## [1.18.1](https://github.com/asepharyana/zesdex/compare/v1.18.0...v1.18.1) (2026-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nix:** restrict flake to x86_64-linux (nixpkgs 26.11 dropped darwin) ([0e0fd5d](https://github.com/asepharyana/zesdex/commit/0e0fd5df05e53a6fa17d481825f1a8c567214d0e))
|
||||
|
||||
# [1.18.0](https://github.com/asepharyana/zesdex/compare/v1.17.0...v1.18.0) (2026-07-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add .releaserc.json for semantic-release configuration ([047d718](https://github.com/asepharyana/zesdex/commit/047d7183d72adaa0dd1a18a7f92b22d67d5aa85f))
|
||||
* **clippy:** replace type annotation with type alias + .insert() to avoid trivial_cast ([ffefa8c](https://github.com/asepharyana/zesdex/commit/ffefa8c07facbba83778f426ca4715569556b08a))
|
||||
* **rust:** remove unused imports, variables, and dead code causing CI build failures ([eb8cc17](https://github.com/asepharyana/zesdex/commit/eb8cc1799359a5ef5b4a4ee3a4202b87e80bfe6c))
|
||||
* **rust:** resolve all clippy warnings treated as errors in CI ([6b90c7e](https://github.com/asepharyana/zesdex/commit/6b90c7eb0d17d9281a377454e76dcc88a4232bc7))
|
||||
* **tui:** resolve remaining clippy errors in workspace ([eed4025](https://github.com/asepharyana/zesdex/commit/eed4025918de65eb2f3fd98cd3350d62f3322132))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add semantic search tool for code symbol indexing and searching ([fef3c92](https://github.com/asepharyana/zesdex/commit/fef3c925cd02e1d76e2d990cc6fa4e4cba64fab0))
|
||||
* Add SOLID principles and TDD reference documentation ([55677dd](https://github.com/asepharyana/zesdex/commit/55677dd67100813a5ed2b0bbf02257b57875ea07))
|
||||
* add test_load and test_parse binaries for configuration loading and parsing ([fe2e916](https://github.com/asepharyana/zesdex/commit/fe2e9169371bf7a3cc9391d8467d47fc2258ad1f))
|
||||
* **agent:** add AI summarization for conversation history compacting ([873f870](https://github.com/asepharyana/zesdex/commit/873f870e233dd0d2c6f0cce935f83fb8ce373505))
|
||||
* **agent:** implement agent execution engine and turn handling with background processing ([efcd191](https://github.com/asepharyana/zesdex/commit/efcd191f9698023944f9096a86c1efa47e412b6b))
|
||||
* **best_practice:** add code quality scanning and commit message validation ([8ecc588](https://github.com/asepharyana/zesdex/commit/8ecc588a3e125dc6a4b2e7eb4c12c6b344449c07))
|
||||
* centralize default constants and refactor overlay enter handling in TUI ([d615090](https://github.com/asepharyana/zesdex/commit/d615090dcd34bdb93be22c1c7ce4ba674363c258))
|
||||
* enhance context gathering in auto-review engine and agent runner ([dcc8c3e](https://github.com/asepharyana/zesdex/commit/dcc8c3ee42ef568843e04143048d799bc0882288))
|
||||
* enhance explore phase with TUI workflow event handling ([b3a4d13](https://github.com/asepharyana/zesdex/commit/b3a4d131d13d94bd307720cb50c2d02dedf92873))
|
||||
* implement Component trait for modular UI components and refactor TUI views to use it ([d59713d](https://github.com/asepharyana/zesdex/commit/d59713d3e3aa4325f9753bbddd068654e442f6ef))
|
||||
* implement mandatory explore phase with parallel subagents ([f368f3a](https://github.com/asepharyana/zesdex/commit/f368f3a1c0d2df02eb50542a81bcff96d9cdc420))
|
||||
* **llm:** improve tool call handling by dynamically resizing tool_calls and updating arguments ([9bfb95d](https://github.com/asepharyana/zesdex/commit/9bfb95d795d6c7cc10cfcb907642bba7b8bb7f19))
|
||||
* **llm:** improve UTF-8 handling in response processing to prevent infinite loops ([7ea2265](https://github.com/asepharyana/zesdex/commit/7ea226509c8e11ee1bab21d216e0413715988fb9))
|
||||
* **llm:** increase max retries for streaming requests from 3 to 10 ([6c7995f](https://github.com/asepharyana/zesdex/commit/6c7995f5f5481c48ccd449e72d9563425fdcb837))
|
||||
* **mcp:** enhance MCP server registration with error handling and improve transport process management ([148ba4e](https://github.com/asepharyana/zesdex/commit/148ba4e07b424736c6b4824c5786c0c7c9cbf0a1))
|
||||
* refactor auto-review engine to use spawn_subagent for improved thread handling ([094eb4b](https://github.com/asepharyana/zesdex/commit/094eb4b8baa5abd878dcf5b611cf615955bc79eb))
|
||||
* remove obsolete design documents for clipboard OSC52, diff view, file mention, context compaction, and add development guide ([66ac4db](https://github.com/asepharyana/zesdex/commit/66ac4dbf027820697f4303264666b5f63a0b31d4))
|
||||
* **tui:** add rich context information including active jobs, README snippet, and recent git history ([2f5f62a](https://github.com/asepharyana/zesdex/commit/2f5f62ab097614d8b0c7a3e5c7bd364cd479e0aa))
|
||||
* **tui:** add support for reasoning in chat messages and update transcript handling ([285dbb1](https://github.com/asepharyana/zesdex/commit/285dbb14cccb1c261134c1fcb6c30ef1ed02bb55))
|
||||
* **tui:** enhance agent turn handling by grouping parameters and improving message management ([07b217c](https://github.com/asepharyana/zesdex/commit/07b217cf4a81b99c63101512932935246fcbc7ae))
|
||||
* **tui:** enhance system prompt with workspace structure information ([bed9f8c](https://github.com/asepharyana/zesdex/commit/bed9f8cff60711c4db998c8f10f32f8e210ae410))
|
||||
* **tui:** implement streaming support for LLM responses and update transcript handling ([cac6626](https://github.com/asepharyana/zesdex/commit/cac662658651ae092cffddfc88f39a775afef942))
|
||||
* **tui:** integrate rich context builder into agent turn process ([919435e](https://github.com/asepharyana/zesdex/commit/919435eb84f9a41a0637052a5dc1234b410714f8))
|
||||
* **tui:** introduce comprehensive state management for TUI interface ([8c58faf](https://github.com/asepharyana/zesdex/commit/8c58faf2920b7341dd50ace15044ff113cefb576))
|
||||
* **tui:** optimize performance by caching display lines and token counts, and improve action handling ([87abe8c](https://github.com/asepharyana/zesdex/commit/87abe8c3358ac17196df93d19169901a856850d3))
|
||||
* **tui:** update system message for clarity and conciseness in tool usage instructions ([5a373d1](https://github.com/asepharyana/zesdex/commit/5a373d1031b476970bd45804a65c7455be99e78d))
|
||||
|
||||
|
||||
# [1.17.0](https://github.com/asepharyana/zesdex/compare/v1.16.1...v1.17.0) (2026-07-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **token:** add refresh token verification to TokenService ([a046519](https://github.com/asepharyana/zesdex/commit/a04651905f4afd562b516c839449a2c813ce6627))
|
||||
|
||||
## [1.16.1](https://github.com/asepharyana/zesdex/compare/v1.16.0...v1.16.1) (2026-07-20)
|
||||
|
||||
# [1.16.0](https://github.com/asepharyana/zesdex/compare/v1.15.2...v1.16.0) (2026-07-20)
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
Tests use `#[cfg(test)] mod tests` blocks inline in production files (not a separate `tests/` dir).
|
||||
|
||||
Tracing output goes to `~/.local/share/zesdex/zesdex.log`. Set `RUST_LOG=debug` for verbose logging.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Zesdex is an autonomous AI coding agent with a TUI — an OpenAI/Anthropic-compatible LLM client wrapped in a tool-use harness with 37 built-in tools.
|
||||
|
||||
Detailed architecture documentation is in `docs/CODEMAPS/`:
|
||||
|
||||
| File | Covers |
|
||||
|------|--------|
|
||||
| [`docs/CODEMAPS/architecture.md`](docs/CODEMAPS/architecture.md) | System layout, process modes, data flow, key files |
|
||||
| [`docs/CODEMAPS/backend.md`](docs/CODEMAPS/backend.md) | Provider, OAuth, IPC, workflow engine, MCP, review, bg bash |
|
||||
| [`docs/CODEMAPS/frontend.md`](docs/CODEMAPS/frontend.md) | TUI render pipeline, 16 overlays, toasts, input handling |
|
||||
| [`docs/CODEMAPS/data.md`](docs/CODEMAPS/data.md) | Persistence, SQLite msglog, memory files, settings/config |
|
||||
| [`docs/CODEMAPS/dependencies.md`](docs/CODEMAPS/dependencies.md) | 23 Rust crates, 5 external services |
|
||||
|
||||
`docs/runs/` holds an auto-generated audit trail: one markdown file per hive-mind convergence (see below), written deterministically by `app::workflow::docs::write_hive_mind_convergence` — not hand-maintained like `docs/CODEMAPS/`.
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **State mutation** — `AppStateRest` is mutable in-place from `actions/mod.rs` and `controller/input.rs`. No generic update function.
|
||||
- **No DI** — modules call `Settings::load()`, `AppConfig::load()`, `all_tools()` directly.
|
||||
- **Logging** — `tracing::warn!` to `~/.local/share/zesdex/zesdex.log` (not stderr, avoids TUI corruption).
|
||||
- **Error handling** — `anyhow::Result` and `anyhow::bail!` throughout. No custom error types.
|
||||
- **Static strings** — MCP tool descriptions use `Box::leak` + `OnceLock` cache.
|
||||
- **Tools** — `trait Tool { fn name() -> &str, fn run() -> Result<String> }`, 28 impls, gated by `Harness`.
|
||||
- **Shell safety** — `tool/shell_filter/` blocks destructive git commands (`shell_filter::git::check_git_destructive`, called from `tool/shell.rs::Bash::run`). It also contains a `check_credential_read` detector for credential-file reads, but that one is intentionally NOT wired into `Bash::run` today — see the doc comment on `Bash::run` for why.
|
||||
|
||||
### Hive-Mind Orchestration (Machine Intelligence)
|
||||
|
||||
- **A single Core Intelligence spawning anonymous processing nodes.** The Core Intelligence (main agent) compiles a cognitive cycle plan per task: an ordered list of cycles, each cycle a set of processing nodes that run in parallel. Each node's sole identity is its directive (what to do) and an access tier. Cycle count and nodes-per-cycle are entirely Core-Intelligence output.
|
||||
- **Access tiers** in `src/app/subagent/division.rs` (`tool_scope` module): tool access is granted per node via one of three tiers (`read` / `write` / `full`, see `tool_scope::tools_for`) picked by the Core Intelligence based on what each node's directive actually needs.
|
||||
- **Orchestrator** in `src/app/workflow/hive_mind.rs`: `run_hive_mind()` executes a `CognitiveCyclePlan { cycles: Vec<Vec<NodeDirective>> }` cycle-by-cycle. Node IDs are system-assigned coordinates (e.g. `"Node-0-1"`).
|
||||
- **Continuous collective state, not phase-boundary sync**: `engine::execute_primitive`'s `ScopedAgent` arm merges each node's complete output into the shared collective-state channel the instant that node finishes — not after its whole parallel cohort completes — so sibling/later nodes see it in real time.
|
||||
- **Consensus synthesis, not a per-node summary**: after all cycles complete, `synthesize_consensus()` spawns one final read-only node whose sole directive is to reconcile the entire collective state into a single consensus assessment — a real reasoning pass, not string concatenation, since node outputs can overlap or conflict.
|
||||
- **Auto-trigger** in `run_agent_turn()` (`actions/mod.rs`): `is_complex_request()` heuristics decide only whether to ask the Core Intelligence to compile a plan at all — the plan's shape is fully dynamic.
|
||||
- **`hive_mind` tool** (`src/tool/workflow.rs`) is the manual entry point: the calling LLM supplies its own `cycles` array of `{directive, access}` directly.
|
||||
- **Guaranteed documentation**: after every convergence, `src/app/workflow/docs.rs::write_hive_mind_convergence()` deterministically (not an LLM step, not skippable) writes every node's full output plus the final consensus to `docs/runs/<timestamp>-<slug>.md`.
|
||||
- **Live node progress** in TUI panel (`view/workflow.rs`): shows node designation + current tool via `AgentStatus::progress`.
|
||||
- **Auto inline review** after each edit: `src/app/subagent/auto.rs` — `spawn_quick_review()` injects verdict back into LLM conversation.
|
||||
- **Background subagents** (test-gen, arch-review, security-review) fire asynchronously at turn end via `TurnEvent::SystemNote`, retrying once on failure and escalating to a blocking (`ESCALATED:`-prefixed, `ToastKind::Error`) notice if the retry also fails.
|
||||
|
||||
Commit convention (Conventional Commits, Bahasa Indonesia): see the `commit-convention` skill.
|
||||
|
||||
## Code Documentation
|
||||
|
||||
Every function, struct, enum, trait, module, and significant code block must have a doc comment (`///` or `//!`) that explains:
|
||||
|
||||
- **What** the function/module does (purpose, not how)
|
||||
- **Flow** — a brief ASCII or prose description of the code flow / data flow above each non-trivial function
|
||||
- **Why** — non-obvious decisions, edge cases, invariants
|
||||
- **Return** — what the caller gets back, especially for `Result` types
|
||||
|
||||
Examples:
|
||||
|
||||
```rust
|
||||
/// Parse an SSE data chunk into one or more StreamEvents.
|
||||
///
|
||||
/// Flow: buffer → split on '\n' → flush on blank line → JSON parse → match event type
|
||||
/// → return Token / ToolCallDelta / Usage / Done.
|
||||
///
|
||||
/// Edge case: chunk may split mid-line; remaining bytes stay in buffer
|
||||
/// for the next feed() call.
|
||||
fn feed(&mut self, chunk: &str) -> Vec<StreamEvent> { ... }
|
||||
|
||||
/// The single source-of-truth state struct for the entire application.
|
||||
///
|
||||
/// Mutated in-place from two locations: actions/mod.rs (apply_action)
|
||||
/// and controller/input.rs (key event handlers). Read-only from
|
||||
/// every other module.
|
||||
struct AppStateRest { ... }
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Every `pub fn` needs a doc comment
|
||||
- Every `pub struct` / `pub enum` / `pub trait` needs a doc comment
|
||||
- Non-trivial private functions (≥10 lines) need a doc comment
|
||||
- Write the comment above the code it documents (not inline in the body)
|
||||
- Update comments when code behavior changes — stale docs are worse than no docs
|
||||
- NEVER use compiler/linter bypass annotations or attributes (such as `#[allow(clippy::too_many_lines, clippy::too_many_arguments, clippy::ref_option)]`, `#[allow(dead_code)]`, etc.) to silence warnings or skip linter checks. Always fix the underlying code issues instead.
|
||||
Generated
+11
-11
@@ -4895,7 +4895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-api"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -4918,7 +4918,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-application"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4935,7 +4935,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-bootstrap"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -4952,7 +4952,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-daemon"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4976,7 +4976,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-domain"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4992,7 +4992,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-gateway"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5019,7 +5019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-grpc"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5036,7 +5036,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-infrastructure"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -5085,7 +5085,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-tui"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -5111,7 +5111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-web"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5131,7 +5131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-ws"
|
||||
version = "1.16.1"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.16.1"
|
||||
version = "1.18.3"
|
||||
edition = "2021"
|
||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Build with release profile (treats warnings as errors via lints)
|
||||
RUN cargo build --release -p zesdex-backend --bin zesdex
|
||||
RUN cargo build --release -p zesdex-gateway --bin zesdex
|
||||
|
||||
# Stage 2: Minimal runtime image
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -1,379 +1,250 @@
|
||||
# Zesdex
|
||||
# Zesdex — Autonomous AI Coding Agent
|
||||
|
||||
> Autonomous AI coding agent in a terminal-based TUI.
|
||||
Zesdex is an autonomous AI coding agent with a Terminal UI (TUI). It acts as an
|
||||
OpenAI/Anthropic-compatible LLM client wrapped in a tool-use harness with **37
|
||||
built-in tools** — file operations, git, shell execution, LSP integration, MCP,
|
||||
subagent orchestration, and more.
|
||||
|
||||
Zesdex is a Rust-powered AI assistant that operates directly in your terminal via a rich TUI interface. It combines large language model intelligence with a comprehensive set of tools to explore, understand, and modify codebases autonomously — with built-in guardrails at every layer.
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Mode Selector │
|
||||
│ TUI (default) ─── Daemon ─── Attach ─── API ─── WS/gRPC/Web │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
## Quick Start
|
||||
|
||||
### Core
|
||||
```bash
|
||||
# Run the TUI (default mode)
|
||||
cargo run
|
||||
|
||||
- **TUI Interface** — Full-screen terminal UI with chat panel, input bar, and status bar built with [ratatui](https://github.com/ratatui-org/ratatui) and [crossterm](https://github.com/crossterm-rs/crossterm).
|
||||
- **Daemon Architecture** — Run as a background daemon with client attach/detach via Unix domain sockets. The daemon processes state; clients only render.
|
||||
- **IPC Protocol** — Bidirectional state synchronization between daemon and client processes with diff-based updates.
|
||||
- **Provider Agnostic** — Configurable AI model providers with dynamic model selection, per-role temperature/token limits, and API key management.
|
||||
# Run the REST API server
|
||||
cargo run -- --api --api-port 8080
|
||||
|
||||
### Tool System (37 built-in tools)
|
||||
# Run in daemon mode (background + IPC)
|
||||
cargo run -- --daemon
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| **Filesystem** | `read`, `write`, `edit`, `delete` |
|
||||
| **Search** | `grep` (recursive text), `glob` (file patterns) |
|
||||
| **Shell** | `bash`, `bash_output`, `bash_kill` |
|
||||
| **Git** | `git_operator`, `git_worktree`, `git_cred` |
|
||||
| **Memory** | `remember`, `recall`, `forget` |
|
||||
| **Planning** | `plan_enter`, `plan_ready`, `seqthink` |
|
||||
| **Workflow** | `workflow_run`, `note_finding`, `read_findings`, `hive_mind` |
|
||||
| **Utility** | `cd`, `dir_list`, `dir_cache_update`, `pong`, `todowrite`, `todofinish` |
|
||||
| **Agent** | `spawn_agents`, `spawn_pipeline` |
|
||||
| **LSP** | `lsp_connect`, `lsp_diagnostics`, `lsp_hover`, `lsp_completion`, `lsp_definition`, `lsp_references`, `lsp_disconnect` |
|
||||
# Attach TUI to a running daemon session
|
||||
cargo run -- --attach <session-id>
|
||||
|
||||
### Intelligence
|
||||
# Seed initial data (first run)
|
||||
cargo run --bin bootstrap
|
||||
```
|
||||
|
||||
- **Hive-Mind Orchestration** — Autonomous agent orchestration modeled as a distributed machine intelligence (à la Stellaris). The Core Intelligence (main agent) compiles a cognitive cycle plan per task — an ordered list of cycles, each a set of anonymous processing nodes that run in parallel. Every node carries only a directive (what to do) and an access tier (`read`/`write`/`full`); cycle count and nodes-per-cycle are decided per task, not fixed. Every node's output merges into a shared Line truncated
|
||||
### Prerequisites
|
||||
|
||||
- **Workflow Engine** — Orchestrate complex multi-step tasks with parallel sub-agents, pipelines, and phased execution. Spawn independent workers that share findings in real-time.
|
||||
- **Self-Learning** — Persistent memory system that stores lessons, references, and project knowledge across sessions. Memories include provenance tracking, lifecycle management, and scope isolation.
|
||||
- **Self-Review** — Review subagents trigger automatically after each code edit (inline) and at turn completion (background). Three types: code quality, architecture, and security.
|
||||
- **Self-Healing** — On build/test failures, spawns a sub-agent with the error context to autonomously fix issues before reporting them to the user.
|
||||
- **MCP Support** — [Model Context Protocol](https://modelcontextprotocol.io/) integration for connecting to external AI tool servers.
|
||||
- **Sequential Thinking** — Chain-of-thought reasoning tool for step-by-step problem decomposition.
|
||||
- **Session Locking** — Prevents multiple processes from operating on the same session directory.
|
||||
- **Rust** 1.81+ (edition 2021)
|
||||
- **Linux** or **macOS** (Unix domain sockets required for daemon mode)
|
||||
- An **API key** for an OpenAI/Anthropic-compatible LLM provider (set via
|
||||
settings or environment variable)
|
||||
|
||||
### Session Management
|
||||
---
|
||||
|
||||
- Multiple concurrent sessions with history, rewind, and transcript persistence.
|
||||
- Per-session edit logs with full change tracking.
|
||||
- Session archival and summary generation.
|
||||
## Modes
|
||||
|
||||
| Flag | Mode | Description |
|
||||
|------|------|-------------|
|
||||
| *(none)* | **TUI** | Full terminal UI with chat, overlays, and agent loop in one process |
|
||||
| `--daemon` | **Daemon** | Background daemon with IPC socket; clients attach separately |
|
||||
| `--attach <id>` | **Attach** | Connect TUI to an existing daemon session via Unix socket |
|
||||
| `--api` | **REST API** | HTTP server with session management and chat endpoints |
|
||||
| `--ws` | **WebSocket** | WebSocket server for real-time communication |
|
||||
| `--grpc` | **gRPC** | gRPC server for programmatic access |
|
||||
| `--web` | **Web** | Serves the web frontend |
|
||||
| `--api-port`, `--ws-port`, `--grpc-port`, `--web-port` | *(ports)* | Configure server ports (defaults: 8080, 8081, 50051, 3000) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Clean Architecture Layering
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # Entry point: single-process, daemon, or attach mode
|
||||
├── resources.rs # Embedded resources (help text, system prompts)
|
||||
├── app/
|
||||
│ ├── state/ # AppStateRest — immutable-rest state model
|
||||
│ │ ├── rest.rs # Core state struct
|
||||
│ │ ├── types.rs # Overlay, Toast, Origin enums
|
||||
│ │ ├── snapshot.rs # State snapshots for IPC
|
||||
│ │ ├── diff.rs # Diff-based state synchronization
|
||||
│ │ ├── runtime.rs # Runtime state mutations
|
||||
│ │ └── misc.rs # DirCache and miscellaneous state helpers
|
||||
│ ├── runtime/ # Action dispatch and event loop
|
||||
│ │ ├── actions/ # Action enum and apply_action reducer
|
||||
│ │ ├── stream/ # LLM streaming and tool execution
|
||||
│ │ │ └── tools/ # Tool harness integration
|
||||
│ │ │ └── turn.rs # Turn orchestration
|
||||
│ │ ├── event_loop/ # Main event loop and shortsend
|
||||
│ │ ├── commands.rs # Slash command dispatch
|
||||
│ │ └── shortsend.rs # Short-lived async send helper
|
||||
│ ├── mode/ # UI modes and overlays (13 modes)
|
||||
│ │ ├── bash.rs # Bash panel mode
|
||||
│ │ ├── editor.rs # Multi-line editor mode
|
||||
│ │ ├── effort.rs # Effort level selector
|
||||
│ │ ├── help.rs # Help overlay
|
||||
│ │ ├── key_input.rs # Raw key input mode
|
||||
│ │ ├── learning.rs # Lesson management overlay
|
||||
│ │ ├── loading.rs # Loading spinner overlay
|
||||
│ │ ├── mcp.rs # MCP server management
|
||||
│ │ ├── quit_confirm.rs # Quit confirmation dialog
|
||||
│ │ ├── rewind.rs # Session rewind mode
|
||||
│ │ ├── settings.rs # Settings panel
|
||||
│ │ ├── todo.rs # Task list overlay
|
||||
│ │ └── workflow.rs # Workflow visualization
|
||||
│ ├── harness.rs # Tool harness for agent execution
|
||||
│ ├── workflow/ # Workflow engine
|
||||
│ │ ├── script.rs # Workflow script DSL
|
||||
│ │ ├── engine.rs # Workflow executor
|
||||
│ │ ├── hive_mind.rs # Hive-mind orchestrator
|
||||
│ │ └── docs.rs # Deterministic docs/runs/*.md writer
|
||||
│ ├── mcp/ # MCP client manager
|
||||
│ │ └── manager.rs # MCP server lifecycle and tool exposure
|
||||
│ ├── subagent/ # Sub-agent management
|
||||
│ │ ├── spawn.rs # AgentDefinition and spawning
|
||||
│ │ ├── engine.rs # Sub-agent event loop
|
||||
│ │ ├── context.rs # Context construction for sub-agents
|
||||
│ │ └── event.rs # Progress event types
|
||||
│ ├── bgbash/ # Background bash job management
|
||||
│ │ ├── job.rs # Background job handle
|
||||
│ │ └── control.rs # Bash control (bg/fg/kill)
|
||||
│ ├── lsp/ # LSP client management
|
||||
│ │ ├── client.rs # LSP client connection wrapper
|
||||
│ │ └── provisioner.rs # Auto-provisioning of LSP servers
|
||||
│ └── review/ # Self-review quality system
|
||||
├── controller/
|
||||
│ ├── input.rs # Key event → Action mapping
|
||||
│ └── command.rs # Slash command parser
|
||||
├── dto/
|
||||
│ ├── chat/ # Message, ToolCall, Role types
|
||||
│ │ ├── message.rs # Chat message types
|
||||
│ │ ├── tool.rs # Tool call/result types
|
||||
│ │ └── mod.rs
|
||||
│ └── provider/ # AI provider request/response/usage types
|
||||
│ ├── request.rs # Provider request schema
|
||||
│ ├── response.rs # Provider response schema
|
||||
│ └── usage.rs # Token usage tracking
|
||||
├── ipc/
|
||||
│ ├── protocol.rs # ClientRequest, DaemonFrame, StatePayload
|
||||
│ ├── server.rs # Unix socket server
|
||||
│ ├── client.rs # Unix socket client
|
||||
│ ├── conn.rs # Framed connection
|
||||
│ ├── frame.rs # Length-prefixed frame encoding
|
||||
│ ├── snapshot.rs # State snapshot serialization
|
||||
│ └── diff.rs # Binary diff for state sync
|
||||
├── model/
|
||||
│ ├── store.rs # File-based storage (~/.config/zesdex/)
|
||||
│ ├── session.rs # Session CRUD and listing
|
||||
│ ├── settings.rs # User settings (provider, model, tokens)
|
||||
│ ├── app_config.rs # Provider definitions and model roles
|
||||
│ ├── memory.rs # Persistent memory with frontmatter
|
||||
│ ├── editlog.rs # Edit history tracking
|
||||
│ ├── msglog/ # Message log (SQLite-backed)
|
||||
│ │ ├── schema.rs # SQLite schema
|
||||
│ │ ├── query.rs # Query helpers
|
||||
│ │ ├── blobs.rs # Large blob storage
|
||||
│ │ └── summary.rs # Session summarization
|
||||
│ ├── agent_def/ # Agent definitions (builtin, global, session)
|
||||
│ │ ├── builtin.rs # Built-in agent profiles
|
||||
│ │ ├── global.rs # Global agent config
|
||||
│ │ └── session.rs # Per-session agent config
|
||||
│ ├── conversation.rs # Conversation helpers
|
||||
│ └── session_lock.rs # Flock-based session locking
|
||||
├── service/
|
||||
│ ├── provider.rs # AI provider abstraction
|
||||
│ └── oauth/ # OAuth PKCE flow with loopback server
|
||||
│ ├── loopback.rs # Local HTTP server for OAuth redirect
|
||||
│ ├── manager.rs # OAuth token manager
|
||||
│ ├── pkce.rs # PKCE code challenge/verifier
|
||||
│ └── mod.rs
|
||||
├── tool/ # 34 tool implementations
|
||||
│ ├── fs/ # read, write, edit, delete
|
||||
│ │ ├── read.rs
|
||||
│ │ ├── write.rs
|
||||
│ │ ├── edit.rs
|
||||
│ │ ├── delete.rs
|
||||
│ │ └── helpers.rs # Path resolution and validation
|
||||
│ ├── search.rs # grep, glob
|
||||
│ ├── shell.rs # bash
|
||||
│ ├── bash_tools.rs # bash_output, bash_kill
|
||||
│ ├── git_operator.rs # git operations
|
||||
│ ├── git_worktree.rs # git worktree management
|
||||
│ ├── git_cred.rs # git credential store/get/erase
|
||||
│ ├── memory/ # remember, forget, recall
|
||||
│ │ ├── remember.rs
|
||||
│ │ ├── forget.rs
|
||||
│ │ └── recall.rs
|
||||
│ ├── plan.rs # plan_enter, plan_ready
|
||||
│ ├── seqthink.rs # Sequential thinking
|
||||
│ ├── workflow.rs # workflow_run, note_finding
|
||||
│ ├── utility/ # cd, dir_list, dir_cache_update, pong, todowrite, todofinish
|
||||
│ │ ├── cd.rs
|
||||
│ │ ├── dir_list.rs
|
||||
│ │ ├── dir_cache_update.rs
|
||||
│ │ ├── pong.rs
|
||||
│ │ ├── todowrite.rs
|
||||
│ │ └── todofinish.rs
|
||||
│ ├── lsp/ # LSP tools (connect, diagnostics, hover, etc.)
|
||||
│ │ └── mod.rs
|
||||
│ └── shell_filter/ # Shell output filtering (credentials, git)
|
||||
│ ├── credentials.rs
|
||||
│ ├── git.rs
|
||||
│ └── mod.rs
|
||||
└── view/ # TUI rendering
|
||||
├── chat.rs # Chat transcript with markdown
|
||||
├── markdown.rs # Markdown → ratatui spans
|
||||
├── status.rs # Status bar
|
||||
├── theme.rs # Color scheme
|
||||
└── workflow.rs # Workflow visualization
|
||||
apps/
|
||||
├── domain/ # Pure entities, value objects, repository/service traits
|
||||
│ # Zero framework deps — only serde + chrono + uuid
|
||||
├── application/ # Use-case services (auth, sessions, conversations, memory)
|
||||
│ # Depends only on domain-layer trait interfaces
|
||||
├── infrastructure/ # All I/O: LLM client, IPC, persistence, LSP, MCP, tools
|
||||
│ # Implements domain/application port interfaces
|
||||
└── interfaces/ # Entry points
|
||||
├── tui/ # Ratatui terminal UI
|
||||
├── api/ # Axum REST API
|
||||
├── daemon/ # Unix socket daemon + client
|
||||
├── ws/ # WebSocket server
|
||||
├── grpc/ # gRPC server
|
||||
└── web/ # Web frontend (static file server)
|
||||
```
|
||||
|
||||
---
|
||||
### Tool System
|
||||
|
||||
## Usage
|
||||
37 tools across 9 categories:
|
||||
|
||||
```bash
|
||||
# Run in single-process mode (default)
|
||||
zesdex
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| **File System** | `read`, `write`, `edit`, `delete`, `dir_list`, `dir_cache_update` |
|
||||
| **Shell** | `bash`, `bash_interactive`, `bash_kill`, `bash_output` |
|
||||
| **Git** | `git_operator`, `git_cred`, `git_worktree` |
|
||||
| **Search** | `search`, `grep`, `glob`, `semantic_search` |
|
||||
| **LSP** | `lsp_connect`, `lsp_hover`, `lsp_completion`, `lsp_definition`, `lsp_references`, `lsp_diagnostics`, `lsp_disconnect` |
|
||||
| **Memory** | `remember`, `recall`, `forget` |
|
||||
| **Workflow** | `spawn_agents`, `spawn_pipeline`, `plan`, `sequential_think`, `hive_mind` |
|
||||
| **Utility** | `todo_write`, `todo_finish`, `pong`, `cd` |
|
||||
| **Background** | Background bash jobs with `cancel/status/list` |
|
||||
|
||||
# Run as a background daemon
|
||||
zesdex --daemon
|
||||
|
||||
# Attach to a running daemon session
|
||||
zesdex --attach <session-id>
|
||||
|
||||
# Set log level
|
||||
RUST_LOG=debug zesdex
|
||||
```
|
||||
|
||||
### Key Bindings
|
||||
|
||||
| Binding | Action |
|
||||
|---------|--------|
|
||||
| `Ctrl+Q` | Quit |
|
||||
| `Ctrl+H` | Help overlay |
|
||||
| `Ctrl+P` | Settings overlay |
|
||||
| `Ctrl+B` | Bash panel |
|
||||
| `Ctrl+S` | Session hub |
|
||||
| `Ctrl+T` | Task list |
|
||||
| `Ctrl+W` | Workflow view |
|
||||
| `Ctrl+K` | Key input mode |
|
||||
| `Esc` | Cancel / back |
|
||||
| `Tab` | Autocomplete |
|
||||
| `↑/↓` | History / navigation |
|
||||
| `Scroll` | Mouse scroll in chat |
|
||||
|
||||
### Slash Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/help` | Show help |
|
||||
| `/clear` | Clear transcript |
|
||||
| `/model` | Select AI model provider |
|
||||
| `/workflow` | Open the workflow panel |
|
||||
| `/workflow run <script>` | Run a JSON-encoded workflow script |
|
||||
| `/mcp` | Open MCP server manager |
|
||||
| `/mcp add <name> <command>` | Add an MCP server |
|
||||
| `/login [provider]` | Authenticate with a provider |
|
||||
| `/edit [path]` | Open a file/dir in the external editor |
|
||||
| `/compact` | Compact the conversation transcript |
|
||||
| `/lesson` | Interactive lesson/memory review |
|
||||
| `/quit` | Exit application |
|
||||
| `Any text` | Sent to the AI assistant as a prompt |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration lives in `~/.config/zesdex/` (or platform equivalent via the `dirs` crate).
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `settings.json` | Provider selection, model, temperature, max tokens, review settings, workflow concurrency |
|
||||
| `app_config.json` | AI provider definitions (name, API base URL, auth type, default model) |
|
||||
| `memory/` | Persistent lesson and reference storage (Markdown with YAML frontmatter) |
|
||||
| `sessions/` | Per-session transcripts, edit logs, and activity data |
|
||||
| `run/` | Unix domain sockets for daemon mode |
|
||||
|
||||
### Provider Configuration
|
||||
|
||||
Providers are defined in `app_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"my-provider": {
|
||||
"api_base": "https://api.example.com/v1",
|
||||
"api_key_env": "MY_API_KEY",
|
||||
"default_model": "model-name"
|
||||
}
|
||||
},
|
||||
"model_roles": {
|
||||
"default": {
|
||||
"provider": "my-provider",
|
||||
"model": "model-name",
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7
|
||||
}
|
||||
},
|
||||
"default_provider": "my-provider",
|
||||
"default_model": "model-name"
|
||||
Each tool implements the `Tool` trait:
|
||||
```rust
|
||||
pub trait Tool: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn description(&self) -> &'static str;
|
||||
fn parameters(&self) -> Value;
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String>;
|
||||
}
|
||||
```
|
||||
|
||||
### Settings
|
||||
### Hive Mind Orchestration
|
||||
|
||||
Key settings in `settings.json`:
|
||||
The multi-agent orchestration system compiles a **cognitive cycle plan** per
|
||||
task — ordered cycles of parallel processing nodes. Each node has a directive
|
||||
and an **access tier** (`read` / `write` / `full`). Node outputs merge into a
|
||||
shared collective state in real time, and a final **consensus synthesis**
|
||||
produces the unified result.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `review_enabled` | `true` | Enable self-review after tool execution |
|
||||
| `review_max_lessons_per_run` | `5` | Max lessons loaded per review cycle |
|
||||
| `adaptive_review_max_skip` | `3` | Consecutive passes before skipping review |
|
||||
| `verify_command` | `null` | Optional command to verify changes |
|
||||
| `workflow_max_concurrency` | `5` | Max parallel sub-agents in workflows |
|
||||
| `session_archive_enabled` | `true` | Auto-archive completed sessions |
|
||||
- **Auto-trigger**: Complex requests automatically use the hive mind
|
||||
- **Manual entry**: The `hive_mind` tool lets the LLM specify cycles explicitly
|
||||
- **Live progress**: TUI panel shows each node's status and current tool
|
||||
- **Guaranteed docs**: Every convergence writes to `docs/runs/`
|
||||
|
||||
### IPC Protocol (Daemon Mode)
|
||||
|
||||
```
|
||||
┌──────────┐ Unix socket ┌──────────┐
|
||||
│ Client │ ◄──────────────► │ Daemon │
|
||||
│ (TUI) │ length-prefixed│ │
|
||||
└──────────┘ serde_json └──────────┘
|
||||
|
||||
Frame format: [4-byte BE length][JSON payload]
|
||||
```
|
||||
|
||||
The daemon holds `AppStateRest` and drives the agent loop. Clients are stateless
|
||||
renderers that receive full state snapshots after each action.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## Built-in Features
|
||||
|
||||
### Prerequisites
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **LLM Provider** | OpenAI/Anthropic-compatible API (streaming + non-streaming) with automatic retry and fallback |
|
||||
| **Tool Harness** | Safety-gated tool execution with graduated review checks |
|
||||
| **Subagents** | Auto-inline review, background test-gen, arch-review, security-review |
|
||||
| **OAuth 2.0** | PKCE flow for LLM provider authentication |
|
||||
| **MCP** | Model Context Protocol server management (stdio + HTTP transport) |
|
||||
| **LSP** | Language Server Protocol integration (completion, hover, diagnostics, references) |
|
||||
| **Session Mgmt** | SQLite-persisted sessions with lock-based concurrency control |
|
||||
| **Memory** | File-based memory system with frontmatter metadata |
|
||||
| **Edit Log** | Append-only edit history with configurable retention |
|
||||
| **Rate Limiting** | Sliding-window per-client rate limiter |
|
||||
| **JWT Auth** | HS256 JWT access/refresh tokens (API mode) |
|
||||
| **Password Auth** | Argon2 password hashing with pepper |
|
||||
| **OAuth Loopback** | Localhost HTTP server for OAuth redirect capture |
|
||||
| **Background Jobs** | Long-running shell jobs with cancellation and output collection |
|
||||
| **Settings** | JSON-persisted settings with hot-reload |
|
||||
|
||||
- **Rust** 2021 edition toolchain ([rustup](https://rustup.rs/))
|
||||
---
|
||||
|
||||
### Build from Source
|
||||
## TUI Overlays
|
||||
|
||||
16 overlays accessible from the terminal UI:
|
||||
|
||||
| Overlay | Purpose |
|
||||
|---------|---------|
|
||||
| Chat Input | Main input bar with autocomplete |
|
||||
| Bash Panel | Interactive shell panel |
|
||||
| File Editor | Built-in file editor |
|
||||
| Effort Selector | LLM reasoning effort selector |
|
||||
| Help | Keybindings reference |
|
||||
| Key Input | Custom key binding configuration |
|
||||
| Learning | Lesson viewer |
|
||||
| Loading | Generating spinner |
|
||||
| MCP Manager | MCP server management |
|
||||
| Model Selector | LLM model picker |
|
||||
| Quit Confirm | Exit confirmation dialog |
|
||||
| Rewind | Message/history rewind |
|
||||
| Settings | Settings panel |
|
||||
| Todo | Task/TODO list |
|
||||
| Usage | Token usage statistics |
|
||||
| Workflow | Hive-mind node progress |
|
||||
|
||||
---
|
||||
|
||||
## Data & Persistence
|
||||
|
||||
All data lives under the platform's data directory (`~/.local/share/zesdex/`):
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd zesdex
|
||||
cargo build --release
|
||||
./target/release/zesdex
|
||||
```
|
||||
|
||||
~/.local/share/zesdex/
|
||||
├── settings.json # User settings (provider, model, keys)
|
||||
├── app_config.json # Provider definitions (endpoints, env vars)
|
||||
├── sessions/ # Chat sessions (one subdirectory per session)
|
||||
│ └── <uuid>/
|
||||
│ ├── session.json # Session metadata
|
||||
│ ├── messages.jsonl # Message log
|
||||
│ └── .lock # Session lock file
|
||||
└── memories/ # Memory files with frontmatter metadata
|
||||
└── *.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Commit Convention
|
||||
```bash
|
||||
# Build all crates
|
||||
cargo build
|
||||
|
||||
Project ini menggunakan **Conventional Commits** untuk otomatis menentukan versi rilis (melalui semantic-release).
|
||||
# Run all unit tests (8 tests across 11 crates)
|
||||
cargo test
|
||||
|
||||
Format:
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
# Run clippy linting
|
||||
cargo clippy --all-targets
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
# Run with verbose logging
|
||||
RUST_LOG=debug cargo run
|
||||
```
|
||||
|
||||
**`<type>` — menentukan bump version:**
|
||||
### Workspace Crates
|
||||
|
||||
| Type | Bump | Keterangan |
|
||||
|-------------|---------|-------------------------------------------|
|
||||
| `feat` | minor | Fitur baru |
|
||||
| `fix` | patch | Perbaikan bug |
|
||||
| `chore` | patch | Tugas maintenance, refactor ringan |
|
||||
| `docs` | patch | Perubahan dokumentasi |
|
||||
| `refactor` | patch | Refactor kode (tanpa perubahan fungsional)|
|
||||
| `test` | patch | Penambahan atau perbaikan test |
|
||||
| `style` | patch | Perubahan formatting, whitespace, dll |
|
||||
| `perf` | patch | Optimasi performa |
|
||||
| `ci` | patch | Perubahan CI/CD |
|
||||
| Crate | Path | Layer |
|
||||
|-------|------|-------|
|
||||
| `zesdex-domain` | `apps/domain/` | Pure domain entities & traits |
|
||||
| `zesdex-application` | `apps/application/` | Use-case services |
|
||||
| `zesdex-infrastructure` | `apps/infrastructure/` | All I/O & tool implementations |
|
||||
| `zesdex-tui` | `apps/interfaces/tui/` | Ratatui terminal interface |
|
||||
| `zesdex-api` | `apps/interfaces/api/` | Axum REST API |
|
||||
| `zesdex-daemon` | `apps/interfaces/daemon/` | Unix socket daemon |
|
||||
| `zesdex-ws` | `apps/interfaces/ws/` | WebSocket server |
|
||||
| `zesdex-grpc` | `apps/interfaces/grpc/` | gRPC server |
|
||||
| `zesdex-web` | `apps/interfaces/web/` | Web frontend |
|
||||
| `zesdex-gateway` | `apps/gateway/` | CLI entry point & dispatcher |
|
||||
| `zesdex-bootstrap` | `apps/bootstrap/` | Initial data seeder |
|
||||
|
||||
**`BREAKING CHANGE`** pada body commit → **major** (apa pun typenya).
|
||||
### Code Map
|
||||
|
||||
Contoh:
|
||||
```
|
||||
feat(agent): add workspace-aware file search
|
||||
Detailed architecture documentation is in `docs/CODEMAPS/`:
|
||||
|
||||
Implement context-aware search scoped to current workspace directory.
|
||||
|
||||
BREAKING CHANGE: search results now filter by workspace scope.
|
||||
```
|
||||
|
||||
```
|
||||
fix(ipc): handle partial frame on unix socket reconnect
|
||||
```
|
||||
|
||||
```
|
||||
chore: update rustls to 0.23
|
||||
```
|
||||
|
||||
### Release Workflow
|
||||
|
||||
Push ke branch `main` akan memicu:
|
||||
1. **CI** — `cargo build --release` + `cargo test`
|
||||
2. **Semantic Release** — analisis commit → update `Cargo.toml` + `CHANGELOG.md` → git tag → GitHub Release dengan binary
|
||||
| File | Covers |
|
||||
|------|--------|
|
||||
| `docs/CODEMAPS/architecture.md` | System layout, process modes, data flow |
|
||||
| `docs/CODEMAPS/backend.md` | Provider, OAuth, IPC, workflow engine, MCP, LSP, review |
|
||||
| `docs/CODEMAPS/frontend.md` | TUI render pipeline, 16 overlays, toasts, input handling |
|
||||
| `docs/CODEMAPS/data.md` | Persistence, SQLite msglog, memory files, settings/config |
|
||||
| `docs/CODEMAPS/dependencies.md` | All Rust crates and external services |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE) for details.
|
||||
See `CHANGELOG.md` for release history.
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Mandatory explore phase — spawns parallel subagents to discover context
|
||||
//! before the main agent begins its turn.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! Before the main agent's LLM loop, [`ExploreService::explore`] dispatches
|
||||
//! at least 3 subagents in parallel (code-structure scan, symbol-index query,
|
||||
//! semantic-context search). Their findings are consolidated into a single
|
||||
//! system message that is prepended to the conversation.
|
||||
//!
|
||||
//! # Why mandatory
|
||||
//!
|
||||
//! Without structured exploration the main agent works from an empty context
|
||||
//! window. The explore phase guarantees that every turn starts with a compact
|
||||
//! snapshot of what the codebase contains and where relevant code lives.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::collections::VecDeque;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use zesdex_domain::agent::TurnEvent;
|
||||
|
||||
/// The consolidated output of an explore phase — a set of system-level
|
||||
/// context messages injected before the main agent prompt.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExploreOutput {
|
||||
/// One or more system messages summarising what the explore subagents
|
||||
/// discovered. Prepended to the conversation by the turn service.
|
||||
pub context_messages: Vec<String>,
|
||||
/// Short human-readable summary of what was explored.
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
/// Service trait for the mandatory pre-turn exploration phase.
|
||||
///
|
||||
/// Implementors spawn ≥3 parallel subagents, each analysing a different
|
||||
/// aspect of the workspace, and return a consolidated summary.
|
||||
///
|
||||
/// # Object safety
|
||||
///
|
||||
/// This trait is `dyn`-safe — it returns `Pin<Box<dyn Future>>` so it can
|
||||
/// be stored as `Arc<dyn ExploreService>`.
|
||||
pub trait ExploreService: Send + Sync {
|
||||
/// Run the explore phase.
|
||||
///
|
||||
/// `query` — the user's current input phrase.
|
||||
/// `workspace_root` — absolute path to the workspace root.
|
||||
/// `turn_events` — shared event queue for TUI updates.
|
||||
/// Returns structured context messages and a summary blob.
|
||||
fn explore<'a>(
|
||||
&'a self,
|
||||
query: &'a str,
|
||||
workspace_root: &'a str,
|
||||
turn_events: &'a Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ExploreOutput>> + Send + 'a>>;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use anyhow::Result;
|
||||
use std::future::Future;
|
||||
|
||||
use zesdex_domain::agent::AgentTurnParams;
|
||||
|
||||
/// Interface for dispatching tool calls to their concrete implementations.
|
||||
pub trait ToolExecutor: Send + Sync {
|
||||
/// Execute a tool call asynchronously.
|
||||
fn execute(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> impl Future<Output = Result<String>> + Send;
|
||||
}
|
||||
|
||||
/// Service for running agent turns asynchronously.
|
||||
pub trait AgentTurnService: Send + Sync {
|
||||
/// Run a full agent turn loop asynchronously.
|
||||
fn run_turn(&self, params: AgentTurnParams) -> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
pub mod explore;
|
||||
pub mod turn_service;
|
||||
|
||||
pub use explore::{ExploreOutput, ExploreService};
|
||||
pub use turn_service::{compact_messages_with_ai, AgentTurnServiceImpl};
|
||||
@@ -0,0 +1,372 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use zesdex_domain::agent::{AgentTurnParams, TurnEvent};
|
||||
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
|
||||
use zesdex_domain::main_agent_prompt;
|
||||
|
||||
use super::{ExploreService, ToolExecutor};
|
||||
use crate::ports::ProviderService;
|
||||
|
||||
/// Maximum tool-call iterations per agent turn before forcing termination.
|
||||
const MAX_TURN_ITERATIONS: u32 = 50;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a TurnEvent onto the shared queue.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn push_event(queue: &Arc<Mutex<VecDeque<TurnEvent>>>, event: TurnEvent) {
|
||||
if let Ok(mut q) = queue.lock() {
|
||||
q.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: stream-event callback that forwards tokens to the turn-event queue
|
||||
// and checks the abort flag on each emission.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn make_stream_callback(
|
||||
abort: &Arc<AtomicBool>,
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) -> Box<dyn FnMut(&StreamEvent) -> bool + Send> {
|
||||
let abort_clone = Arc::clone(abort);
|
||||
let events_clone = Arc::clone(turn_events);
|
||||
Box::new(move |event: &StreamEvent| -> bool {
|
||||
if abort_clone.load(Ordering::SeqCst) {
|
||||
return false;
|
||||
}
|
||||
match event {
|
||||
StreamEvent::Token(s) => {
|
||||
push_event(&events_clone, TurnEvent::StreamToken(s.clone()));
|
||||
}
|
||||
StreamEvent::Reasoning(s) => {
|
||||
push_event(&events_clone, TurnEvent::StreamReasoning(s.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: execute a single tool call, push events, return the result string.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn execute_tool_call<T: ToolExecutor>(
|
||||
tool_executor: &T,
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
tc: &zesdex_domain::core::ToolCall,
|
||||
) -> String {
|
||||
let name = &tc.function.name;
|
||||
let args = zesdex_domain::core::tool_call::sanitize_tool_arguments(&tc.function.arguments);
|
||||
|
||||
debug!("executing tool: {name}");
|
||||
|
||||
let output = match tool_executor.execute(name, &args).await {
|
||||
Ok(o) => o,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
};
|
||||
|
||||
let is_error = output.starts_with("Error:");
|
||||
|
||||
push_event(
|
||||
turn_events,
|
||||
TurnEvent::ToolResult {
|
||||
tool_call_id: tc.id.clone(),
|
||||
tool_name: name.clone(),
|
||||
output: output.clone(),
|
||||
is_error,
|
||||
path: None,
|
||||
},
|
||||
);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: emit usage event from optional LLM response metadata.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn emit_usage(turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>, usage: Option<(u64, u64)>) {
|
||||
if let Some((tokens_in, tokens_out)) = usage {
|
||||
push_event(
|
||||
turn_events,
|
||||
TurnEvent::Usage {
|
||||
tokens_in,
|
||||
tokens_out,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Service implementation for executing an agent turn asynchronously.
|
||||
///
|
||||
/// # Explore phase
|
||||
///
|
||||
/// Before the main LLM loop begins, [`AgentTurnServiceImpl`] runs a mandatory
|
||||
/// explore phase that spawns ≥3 parallel subagents (code structure, symbol
|
||||
/// index, semantic context) and injects their consolidated findings as a
|
||||
/// system message. See [`ExploreService`] for the trait contract.
|
||||
pub struct AgentTurnServiceImpl<P: ProviderService, T: ToolExecutor> {
|
||||
provider: Arc<P>,
|
||||
tool_executor: Arc<T>,
|
||||
tool_defs: Vec<ToolDef>,
|
||||
/// Optional explore-phase service. When `Some`, the explore phase runs
|
||||
/// before every turn; when `None` it is skipped (tests, daemon mode).
|
||||
explore_service: Option<Arc<dyn ExploreService>>,
|
||||
}
|
||||
|
||||
impl<P: ProviderService, T: ToolExecutor> AgentTurnServiceImpl<P, T> {
|
||||
pub fn new(provider: Arc<P>, tool_executor: Arc<T>, tool_defs: Vec<ToolDef>) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
tool_executor,
|
||||
tool_defs,
|
||||
explore_service: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach an optional explore-phase service.
|
||||
///
|
||||
/// When set, every call to `run_turn` will first run the explore phase
|
||||
/// and inject the consolidated context as a system message.
|
||||
pub fn with_explore(mut self, service: Arc<dyn ExploreService>) -> Self {
|
||||
self.explore_service = Some(service);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute a single LLM call with the current message list, handling
|
||||
/// streaming events and error reporting.
|
||||
async fn call_llm(
|
||||
&self,
|
||||
messages: &[ChatMessage],
|
||||
abort: &Arc<AtomicBool>,
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) -> Result<(ChatMessage, Option<(u64, u64)>), String> {
|
||||
let on_event = make_stream_callback(abort, turn_events);
|
||||
|
||||
self.provider
|
||||
.chat_stream(
|
||||
messages,
|
||||
Some(self.tool_defs.clone()),
|
||||
Some(4096),
|
||||
Some(0.7),
|
||||
on_event,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("LLM error: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnServiceImpl<P, T> {
|
||||
async fn run_turn(&self, mut params: AgentTurnParams) -> anyhow::Result<()> {
|
||||
info!(
|
||||
"Starting async agent turn with {} messages (model: {})",
|
||||
params.messages.len(),
|
||||
params.model
|
||||
);
|
||||
|
||||
// ── Phase 0: Mandatory explore ──────────────────────────────────
|
||||
// Spawn ≥3 parallel subagents to discover code structure, symbols,
|
||||
// and semantic context. The consolidated summary is injected as a
|
||||
// system message before the main agent prompt.
|
||||
if let Some(ref explorer) = self.explore_service {
|
||||
// Determine workspace root from the first message's context or
|
||||
// the first workspace root in params.
|
||||
let user_query = params
|
||||
.messages
|
||||
.last()
|
||||
.map(|m| m.content.clone().unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
let workspace_root = params
|
||||
.workspace_roots
|
||||
.first()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "info".into(),
|
||||
message: "🔍 Exploring codebase structure...".into(),
|
||||
},
|
||||
);
|
||||
|
||||
match explorer
|
||||
.explore(&user_query, &workspace_root, ¶ms.turn_events)
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
// Insert each context message as a system message.
|
||||
// They go at index 0 and are removed after the turn
|
||||
// like the main agent prompt.
|
||||
for ctx_msg in &output.context_messages {
|
||||
params
|
||||
.messages
|
||||
.insert(0, ChatMessage::system(ctx_msg.clone()));
|
||||
}
|
||||
info!(
|
||||
"Explore phase complete: {} context messages, {}",
|
||||
output.context_messages.len(),
|
||||
output.summary
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Explore phase failed (non-fatal): {e}");
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "warn".into(),
|
||||
message: format!("Explore phase failed: {e}"),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert system prompt at position 0 once and keep it there for the
|
||||
// entire turn, avoiding per-iteration clones of the full message list.
|
||||
// It is removed before emitting the Compacted event so persistence
|
||||
// does not store the prompt redundantly.
|
||||
params
|
||||
.messages
|
||||
.insert(0, ChatMessage::system(main_agent_prompt()));
|
||||
let original_count = params.messages.len();
|
||||
|
||||
for iteration in 0..MAX_TURN_ITERATIONS {
|
||||
// ── Check abort flag ────────────────────────────────────────
|
||||
if params.abort.load(Ordering::SeqCst) {
|
||||
params.abort.store(false, Ordering::SeqCst);
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "info".into(),
|
||||
message: "Turn aborted by user".into(),
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
debug!("agent turn iteration {iteration}");
|
||||
|
||||
// ── Stream start + call LLM ─────────────────────────────────
|
||||
push_event(¶ms.turn_events, TurnEvent::StreamStart);
|
||||
|
||||
// Uses params.messages directly (sys_msg[0] already in place
|
||||
// from the insert above) — no per-iteration clone needed.
|
||||
let result = self
|
||||
.call_llm(¶ms.messages, ¶ms.abort, ¶ms.turn_events)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok((assistant_msg, usage)) => {
|
||||
let content = assistant_msg.content.clone().unwrap_or_default();
|
||||
let tool_calls = assistant_msg.tool_calls.clone().unwrap_or_default();
|
||||
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::StreamDone(assistant_msg.clone()),
|
||||
);
|
||||
|
||||
emit_usage(¶ms.turn_events, usage);
|
||||
|
||||
// ── No tool calls → assistant is done ──────────────
|
||||
if tool_calls.is_empty() {
|
||||
params.messages.push(ChatMessage::assistant(Some(content)));
|
||||
break;
|
||||
}
|
||||
|
||||
params.messages.push(assistant_msg);
|
||||
|
||||
// ── Execute each tool call ──────────────────────────
|
||||
for tc in &tool_calls {
|
||||
let output =
|
||||
execute_tool_call(self.tool_executor.as_ref(), ¶ms.turn_events, tc)
|
||||
.await;
|
||||
params
|
||||
.messages
|
||||
.push(ChatMessage::tool(tc.id.clone(), output));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("{e}");
|
||||
push_event(¶ms.turn_events, TurnEvent::Error(e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the synthetic sys_msg before shipping events to the TUI
|
||||
// so the transcript shows only the actual user/assistant/tool exchange.
|
||||
let compacted: Vec<ChatMessage> = params.messages.drain(original_count - 1..).collect();
|
||||
push_event(¶ms.turn_events, TurnEvent::Compacted(compacted));
|
||||
push_event(¶ms.turn_events, TurnEvent::Done);
|
||||
params.in_flight.store(false, Ordering::SeqCst);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conversation compaction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Maximum number of recent messages to preserve during compaction.
|
||||
const COMPACT_KEEP_TAIL: usize = 6;
|
||||
|
||||
/// Compacts conversation history using AI summarisation.
|
||||
///
|
||||
/// Flow: if the message count exceeds `KEEP_TAIL + 2`, the oldest messages
|
||||
/// are drained and summarised by the LLM. The summary is inserted as a
|
||||
/// system message at the head of the remaining history.
|
||||
pub async fn compact_messages_with_ai<P: ProviderService>(
|
||||
messages: &mut Vec<ChatMessage>,
|
||||
provider: &P,
|
||||
) -> anyhow::Result<()> {
|
||||
if messages.len() <= COMPACT_KEEP_TAIL + 2 {
|
||||
return Ok(()); // Not enough messages to compact
|
||||
}
|
||||
|
||||
let split_idx = messages.len() - COMPACT_KEEP_TAIL;
|
||||
let evicted: Vec<_> = messages.drain(..split_idx).collect();
|
||||
|
||||
let mut summary_prompt = vec![ChatMessage::system(zesdex_domain::compaction_prompt())];
|
||||
summary_prompt.extend(evicted);
|
||||
summary_prompt.push(ChatMessage::user(
|
||||
"Please summarise our previous conversation above for context continuity.".to_string(),
|
||||
));
|
||||
|
||||
match provider
|
||||
.chat(&summary_prompt, None, Some(1024), Some(0.3))
|
||||
.await
|
||||
{
|
||||
Ok((summary_msg, _)) => {
|
||||
let summary_text = summary_msg
|
||||
.content
|
||||
.unwrap_or_else(|| "Previous context summarised.".to_string());
|
||||
let summary_node = ChatMessage::system(format!(
|
||||
"[AI Summary of Previous Conversation]\n{}",
|
||||
summary_text.trim()
|
||||
));
|
||||
messages.insert(0, summary_node);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("AI summarisation failed during compact, falling back to simple notice: {e}");
|
||||
messages.insert(
|
||||
0,
|
||||
ChatMessage::system(
|
||||
"[Earlier conversation messages compacted to save context window]".to_string(),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,7 @@ use sha2::{Digest, Sha256};
|
||||
/// and clear them after a successful (or failed) flow completion.
|
||||
pub trait OAuthFlowStore: Send + Sync {
|
||||
/// Persist the PKCE code verifier and CSRF state token.
|
||||
fn save_flow_state(
|
||||
&self,
|
||||
verifier: &str,
|
||||
state: &str,
|
||||
) -> Result<(), ServiceError>;
|
||||
fn save_flow_state(&self, verifier: &str, state: &str) -> Result<(), ServiceError>;
|
||||
|
||||
/// Load the stored PKCE code verifier.
|
||||
fn load_verifier(&self) -> Result<String, ServiceError>;
|
||||
@@ -141,12 +137,7 @@ pub struct OAuthUseCase<R, S, E> {
|
||||
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S, E> {
|
||||
/// Create a new OAuth use-case.
|
||||
pub fn new(
|
||||
token_repo: R,
|
||||
flow_store: S,
|
||||
token_exchanger: E,
|
||||
token_path: PathBuf,
|
||||
) -> Self {
|
||||
pub fn new(token_repo: R, flow_store: S, token_exchanger: E, token_path: PathBuf) -> Self {
|
||||
OAuthUseCase {
|
||||
token_repo,
|
||||
flow_store,
|
||||
@@ -156,8 +147,8 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
|
||||
zesdex_domain::auth::OAuthService for OAuthUseCase<R, S, E>
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> zesdex_domain::auth::OAuthService
|
||||
for OAuthUseCase<R, S, E>
|
||||
{
|
||||
fn start_flow(
|
||||
&self,
|
||||
@@ -183,13 +174,9 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
|
||||
"starting OAuth flow",
|
||||
);
|
||||
|
||||
let mut url = url::Url::parse(&config.auth_url)
|
||||
.map_err(|e| {
|
||||
ServiceError::InvalidConfig(format!(
|
||||
"invalid auth_url '{}': {e}",
|
||||
config.auth_url
|
||||
))
|
||||
})?;
|
||||
let mut url = url::Url::parse(&config.auth_url).map_err(|e| {
|
||||
ServiceError::InvalidConfig(format!("invalid auth_url '{}': {e}", config.auth_url))
|
||||
})?;
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("response_type", "code")
|
||||
|
||||
@@ -46,12 +46,11 @@ impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: SessionRepository, L: SessionLockRepository>
|
||||
zesdex_domain::auth::SessionService for SessionServiceImpl<R, L>
|
||||
impl<R: SessionRepository, L: SessionLockRepository> zesdex_domain::auth::SessionService
|
||||
for SessionServiceImpl<R, L>
|
||||
{
|
||||
fn create_session(&self, title: &str) -> Result<Session, ServiceError> {
|
||||
let id = SessionId::new(&Uuid::new_v4().to_string())
|
||||
.map_err(ServiceError::Other)?;
|
||||
let id = SessionId::new(&Uuid::new_v4().to_string()).map_err(ServiceError::Other)?;
|
||||
let title_owned = if title.is_empty() {
|
||||
"New Session".to_string()
|
||||
} else {
|
||||
@@ -59,8 +58,7 @@ impl<R: SessionRepository, L: SessionLockRepository>
|
||||
};
|
||||
let session = Session::new(id.into_string(), title_owned);
|
||||
tracing::debug!(session_id = %session.id, title = %session.title, "creating new session");
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)?;
|
||||
self.session_repo.save_session(&self.base_dir, &session)?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
@@ -73,17 +71,14 @@ impl<R: SessionRepository, L: SessionLockRepository>
|
||||
|
||||
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> {
|
||||
tracing::debug!(session_id = %id, "archiving session");
|
||||
let mut session = self
|
||||
.session_repo
|
||||
.load_session(&self.base_dir, &id)?;
|
||||
let mut session = self.session_repo.load_session(&self.base_dir, &id)?;
|
||||
session.archived = true;
|
||||
let millis = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX);
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)?;
|
||||
self.session_repo.save_session(&self.base_dir, &session)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,7 @@ impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_message(
|
||||
&self,
|
||||
conv: &mut Conversation,
|
||||
msg: ChatMessage,
|
||||
) -> Result<(), ServiceError> {
|
||||
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError> {
|
||||
tracing::debug!("adding message to session {}", conv.session_id);
|
||||
conv.push(msg);
|
||||
let dir = self.session_dir(&conv.session_id);
|
||||
|
||||
@@ -14,8 +14,7 @@ use std::path::PathBuf;
|
||||
use tracing;
|
||||
|
||||
use zesdex_domain::cms::{
|
||||
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings,
|
||||
SettingsRepository,
|
||||
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, SettingsRepository,
|
||||
};
|
||||
|
||||
/// Service implementation for settings and app-config operations.
|
||||
@@ -30,11 +29,7 @@ pub struct SettingsServiceImpl<S, C> {
|
||||
|
||||
impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
|
||||
/// Create a new service with the given repositories and base directory.
|
||||
pub fn new(
|
||||
settings_repo: S,
|
||||
app_config_repo: C,
|
||||
base_dir: impl Into<PathBuf>,
|
||||
) -> Self {
|
||||
pub fn new(settings_repo: S, app_config_repo: C, base_dir: impl Into<PathBuf>) -> Self {
|
||||
tracing::debug!("creating SettingsServiceImpl");
|
||||
Self {
|
||||
settings_repo,
|
||||
@@ -44,8 +39,8 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SettingsRepository, C: AppConfigRepository>
|
||||
zesdex_domain::cms::SettingsService for SettingsServiceImpl<S, C>
|
||||
impl<S: SettingsRepository, C: AppConfigRepository> zesdex_domain::cms::SettingsService
|
||||
for SettingsServiceImpl<S, C>
|
||||
{
|
||||
fn load_settings(&self) -> Result<Settings, ServiceError> {
|
||||
tracing::debug!("loading settings");
|
||||
@@ -60,11 +55,7 @@ impl<S: SettingsRepository, C: AppConfigRepository>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_provider(
|
||||
&self,
|
||||
name: &str,
|
||||
config: &ProviderConfig,
|
||||
) -> Result<(), ServiceError> {
|
||||
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<(), ServiceError> {
|
||||
tracing::debug!("updating provider '{name}'");
|
||||
let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?;
|
||||
app_config
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
//! the use-case logic independent of any specific persistence or infrastructure
|
||||
//! technology.
|
||||
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cms;
|
||||
pub mod ports;
|
||||
@@ -45,7 +46,11 @@ pub use auth::{
|
||||
|
||||
// Re-export CMS use-cases.
|
||||
pub use cms::{
|
||||
conversation_service::ConversationServiceImpl,
|
||||
memory_service::MemoryServiceImpl,
|
||||
conversation_service::ConversationServiceImpl, memory_service::MemoryServiceImpl,
|
||||
settings_service::SettingsServiceImpl,
|
||||
};
|
||||
|
||||
pub use agent::{
|
||||
turn_service::{compact_messages_with_ai, AgentTurnServiceImpl},
|
||||
AgentTurnService, ExploreOutput, ExploreService, ToolExecutor,
|
||||
};
|
||||
@@ -22,11 +22,8 @@ pub trait AuthService: Send + Sync {
|
||||
/// Authenticate a user by verifying a password against a stored hash.
|
||||
///
|
||||
/// Returns `true` if the password matches, `false` otherwise.
|
||||
fn authenticate(
|
||||
&self,
|
||||
password: &str,
|
||||
hash: &str,
|
||||
) -> impl Future<Output = Result<bool>> + Send;
|
||||
fn authenticate(&self, password: &str, hash: &str)
|
||||
-> impl Future<Output = Result<bool>> + Send;
|
||||
|
||||
/// Issue a new access + refresh token pair for the given subject.
|
||||
///
|
||||
|
||||
@@ -23,4 +23,10 @@ pub trait TokenService: Send + Sync {
|
||||
/// Returns `Err` if the token is expired, malformed, or has an
|
||||
/// invalid signature.
|
||||
fn verify_access_token(&self, token: &str) -> Result<String>;
|
||||
|
||||
/// Verify a refresh token and return the embedded subject claim.
|
||||
///
|
||||
/// Returns `Err` if the token is expired, malformed, or has an
|
||||
/// invalid signature.
|
||||
fn verify_refresh_token(&self, token: &str) -> Result<String>;
|
||||
}
|
||||
@@ -16,7 +16,10 @@ fn main() -> anyhow::Result<()> {
|
||||
if !settings_path.exists() {
|
||||
let settings = zesdex_domain::cms::Settings::default();
|
||||
let content = serde_json::to_string_pretty(&settings)?;
|
||||
std::fs::write(&settings_path, content)?;
|
||||
let tmp = store.base_dir.join("settings.json.tmp");
|
||||
std::fs::write(&tmp, &content)?;
|
||||
std::fs::File::open(&tmp)?.sync_all()?;
|
||||
std::fs::rename(&tmp, &settings_path)?;
|
||||
println!(" ✓ Default settings created");
|
||||
} else {
|
||||
println!(" · Settings already exist, skipping");
|
||||
@@ -27,7 +30,10 @@ fn main() -> anyhow::Result<()> {
|
||||
if !config_path.exists() {
|
||||
let config = zesdex_domain::cms::AppConfig::default();
|
||||
let content = serde_json::to_string_pretty(&config)?;
|
||||
std::fs::write(&config_path, content)?;
|
||||
let tmp = store.base_dir.join("app_config.json.tmp");
|
||||
std::fs::write(&tmp, &content)?;
|
||||
std::fs::File::open(&tmp)?.sync_all()?;
|
||||
std::fs::rename(&tmp, &config_path)?;
|
||||
println!(" ✓ Default app_config created");
|
||||
} else {
|
||||
println!(" · App config already exists, skipping");
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Shared default constants used across the application.
|
||||
//!
|
||||
//! Centralising these values eliminates the hardcoded-string duplication
|
||||
//! that existed when every call site provided its own inline fallback.
|
||||
//! Consumers should reference these constants rather than repeating
|
||||
//! the string literals.
|
||||
|
||||
/// Default LLM provider API base URL.
|
||||
pub const DEFAULT_API_BASE: &str = "https://opencode.ai/zen/v1";
|
||||
|
||||
/// Default LLM model identifier.
|
||||
pub const DEFAULT_MODEL: &str = "deepseek-v4-flash-free";
|
||||
|
||||
/// Fallback JWT secret used only when `JWT_SECRET` env var is unset.
|
||||
/// In production this MUST be configured via environment variable.
|
||||
pub const FALLBACK_JWT_SECRET: &str = "dev-secret";
|
||||
|
||||
/// Default context window size (128k tokens).
|
||||
pub const DEFAULT_CONTEXT_WINDOW: usize = 256_000;
|
||||
|
||||
/// Maximum tool-call iterations per agent turn.
|
||||
pub const MAX_TOOL_ITERATIONS: u32 = 50;
|
||||
|
||||
/// Maximum subagent tool-call iterations.
|
||||
pub const MAX_SUBAGENT_ITERATIONS: u32 = 25;
|
||||
|
||||
/// Default LLM request max tokens.
|
||||
pub const DEFAULT_MAX_TOKENS: u32 = 4096;
|
||||
|
||||
/// Default temperature for the main agent.
|
||||
pub const DEFAULT_TEMPERATURE: f64 = 0.7;
|
||||
|
||||
/// Default temperature for compaction / summary calls.
|
||||
pub const DEFAULT_COMPACT_TEMPERATURE: f64 = 0.3;
|
||||
@@ -0,0 +1,260 @@
|
||||
//! Domain types for agent lifecycle: turn events, session runtime, progress
|
||||
//! reporting, prompts, and the agent-turn parameter bundle.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::core::{ChatMessage, ToolCallResult, UsageStats};
|
||||
|
||||
pub mod defaults;
|
||||
pub mod progress;
|
||||
pub mod prompt;
|
||||
|
||||
/// Which kind of caller (main agent vs. subagent vs. reviewer) is
|
||||
/// invoking a tool, used to scope permissions and tag log/output paths.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum Origin {
|
||||
/// The main agent turn loop.
|
||||
Main,
|
||||
/// A spawned subagent (test-gen, arch-review, security-review, etc.).
|
||||
SubAgent,
|
||||
/// The auto-inline review step after an edit.
|
||||
Reviewer,
|
||||
}
|
||||
|
||||
impl Origin {
|
||||
/// Short string tag for this origin, used in filenames and logs.
|
||||
pub fn tag(self) -> String {
|
||||
match self {
|
||||
Origin::Main => "main",
|
||||
Origin::SubAgent => "subagent",
|
||||
Origin::Reviewer => "reviewer",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Severity/category of a toast notification, used to pick its color.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ToastKind {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
Lesson,
|
||||
}
|
||||
|
||||
/// A transient status message shown in the TUI, auto-dismissed after `lifetime_ms`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Toast {
|
||||
pub kind: ToastKind,
|
||||
pub message: String,
|
||||
pub created_at: i64,
|
||||
pub lifetime_ms: u64,
|
||||
}
|
||||
|
||||
impl Toast {
|
||||
/// Create a toast with a default 5-second lifetime, stamped with now.
|
||||
pub fn new(kind: ToastKind, message: String) -> Self {
|
||||
Toast {
|
||||
kind,
|
||||
message,
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
lifetime_ms: 5000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this toast's lifetime has elapsed as of `now_ms`.
|
||||
pub fn expired(&self, now_ms: i64) -> bool {
|
||||
let lifetime = self.lifetime_ms as i64;
|
||||
now_ms - self.created_at > lifetime
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent status for workflow engine progress tracking.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum AgentStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed(String),
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AgentStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AgentStatus::Pending => write!(f, "pending"),
|
||||
AgentStatus::Running => write!(f, "running"),
|
||||
AgentStatus::Completed => write!(f, "completed"),
|
||||
AgentStatus::Failed(msg) => write!(f, "failed: {msg}"),
|
||||
AgentStatus::Cancelled => write!(f, "cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Events emitted onto the turn-event queue while an agent turn runs,
|
||||
/// consumed by the event loop to update state and drive re-renders.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TurnEvent {
|
||||
AssistantMessage(ChatMessage),
|
||||
ToolResult {
|
||||
tool_call_id: String,
|
||||
tool_name: String,
|
||||
output: String,
|
||||
is_error: bool,
|
||||
path: Option<String>,
|
||||
},
|
||||
SystemNote {
|
||||
kind: String,
|
||||
message: String,
|
||||
},
|
||||
StreamStart,
|
||||
StreamToken(String),
|
||||
StreamReasoning(String),
|
||||
StreamDone(ChatMessage),
|
||||
Usage {
|
||||
tokens_in: u64,
|
||||
tokens_out: u64,
|
||||
},
|
||||
ReviewUsage {
|
||||
tokens_in: u64,
|
||||
tokens_out: u64,
|
||||
},
|
||||
Compacted(Vec<ChatMessage>),
|
||||
Error(String),
|
||||
Done,
|
||||
WorkflowAgentUpdate {
|
||||
agent_id: String,
|
||||
agent_name: String,
|
||||
status: AgentStatus,
|
||||
},
|
||||
TodoUpdate(String),
|
||||
PlanUpdate(String),
|
||||
/// Structured progress report from a subagent or workflow node,
|
||||
/// carrying the current tool name and optional step counters.
|
||||
AgentProgress(crate::agent::progress::AgentProgress),
|
||||
}
|
||||
|
||||
/// How a pending tool call should be executed when the turn resumes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExecutionModel {
|
||||
Inline,
|
||||
Deferred,
|
||||
AsyncTokio,
|
||||
}
|
||||
|
||||
/// A tool call awaiting execution, along with which execution model
|
||||
/// (inline, deferred, async) it should run under.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PendingTool {
|
||||
pub tool_name: String,
|
||||
pub args: serde_json::Value,
|
||||
pub execution_model: ExecutionModel,
|
||||
}
|
||||
|
||||
/// Reference to a background bash job tracked in session state.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BashJobRef {
|
||||
pub id: String,
|
||||
pub command: String,
|
||||
pub started_at: i64,
|
||||
pub running: bool,
|
||||
}
|
||||
|
||||
/// Tracks counts of learned patterns by outcome and lifecycle stage.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LessonStats {
|
||||
/// Total number of lessons tracked.
|
||||
pub total: u32,
|
||||
/// User-initiated lessons.
|
||||
pub user: u32,
|
||||
/// Feedback-driven lessons.
|
||||
pub feedback: u32,
|
||||
/// Project-scoped lessons.
|
||||
pub project: u32,
|
||||
/// Reference-scoped lessons.
|
||||
pub reference: u32,
|
||||
/// Currently active lessons.
|
||||
pub active: u32,
|
||||
/// Stale (outdated) lessons.
|
||||
pub stale: u32,
|
||||
/// Contradicted lessons.
|
||||
pub contradicted: u32,
|
||||
/// Human-authored lessons.
|
||||
pub human: u32,
|
||||
/// Verified lessons.
|
||||
pub verified: u32,
|
||||
/// Unverified lessons.
|
||||
pub unverified: u32,
|
||||
}
|
||||
|
||||
/// Per-session runtime state: message history, pending tool queue,
|
||||
/// background bash jobs, lesson/review counters.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionRuntime {
|
||||
pub messages: Vec<ChatMessage>,
|
||||
pub tool_call_results: Vec<ToolCallResult>,
|
||||
pub pending_tool_queue: Vec<PendingTool>,
|
||||
pub bash_jobs: Vec<BashJobRef>,
|
||||
pub subagent_queue: usize,
|
||||
pub edit_count: u32,
|
||||
pub consecutive_empty_reviews: u32,
|
||||
pub session_start: i64,
|
||||
/// Aggregated lesson statistics.
|
||||
pub lessons: LessonStats,
|
||||
pub review_count: u32,
|
||||
pub session_dir: PathBuf,
|
||||
pub usage: UsageStats,
|
||||
pub hive_mind_converged: bool,
|
||||
}
|
||||
|
||||
impl SessionRuntime {
|
||||
pub fn new(session_dir: PathBuf) -> Self {
|
||||
SessionRuntime {
|
||||
messages: Vec::new(),
|
||||
tool_call_results: Vec::new(),
|
||||
pending_tool_queue: Vec::new(),
|
||||
bash_jobs: Vec::new(),
|
||||
subagent_queue: 0,
|
||||
edit_count: 0,
|
||||
consecutive_empty_reviews: 0,
|
||||
session_start: chrono::Utc::now().timestamp_millis(),
|
||||
lessons: LessonStats::default(),
|
||||
review_count: 0,
|
||||
session_dir,
|
||||
usage: UsageStats::default(),
|
||||
hive_mind_converged: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_message(&mut self, msg: ChatMessage) {
|
||||
self.messages.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple ASCII progress display for a long-running operation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProgressState {
|
||||
pub current: u64,
|
||||
pub total: u64,
|
||||
pub message: String,
|
||||
pub start_time: i64,
|
||||
}
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Owned parameters required to spawn and execute an agent turn.
|
||||
pub struct AgentTurnParams {
|
||||
pub messages: Vec<ChatMessage>,
|
||||
pub session_dir: PathBuf,
|
||||
pub workspace_roots: Vec<PathBuf>,
|
||||
pub turn_events: Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
pub in_flight: Arc<AtomicBool>,
|
||||
pub abort: Arc<AtomicBool>,
|
||||
pub api_key: String,
|
||||
pub model: String,
|
||||
pub api_base: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Progress reporting types for long-running agent and subagent operations.
|
||||
//!
|
||||
//! These types are emitted onto the turn-event queue to drive the TUI's
|
||||
//! spinner, progress bar, and agent-status sidebar. They are pure domain
|
||||
//! types with no I/O or framework dependency.
|
||||
|
||||
use crate::agent::AgentStatus;
|
||||
|
||||
/// Describes progress within a single subagent or workflow-node execution.
|
||||
///
|
||||
/// Emitted as a `TurnEvent::AgentProgress` so the UI can show which tool
|
||||
/// the subagent is currently invoking, or which step it has reached.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentProgress {
|
||||
/// Unique identifier for this agent (e.g. `"Node-0-1"`, `"auto-review"`).
|
||||
pub agent_id: String,
|
||||
/// Human-readable display name shown in the TUI sidebar.
|
||||
pub agent_name: String,
|
||||
/// Current lifecycle status.
|
||||
pub status: AgentStatus,
|
||||
/// Optional description of the current tool or step being executed.
|
||||
/// Set to `None` when the agent is not actively executing a tool.
|
||||
pub current_tool: Option<String>,
|
||||
/// Optional progress range: (completed_steps, total_steps).
|
||||
/// When `None`, the agent shows an indeterminate spinner.
|
||||
pub steps: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl AgentProgress {
|
||||
/// Mark this agent as running with an optional tool name.
|
||||
pub fn running(
|
||||
agent_id: impl Into<String>,
|
||||
agent_name: impl Into<String>,
|
||||
current_tool: Option<String>,
|
||||
) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Running,
|
||||
current_tool,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this agent as pending (queued but not yet started).
|
||||
pub fn pending(agent_id: impl Into<String>, agent_name: impl Into<String>) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Pending,
|
||||
current_tool: None,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this agent as completed successfully.
|
||||
pub fn completed(agent_id: impl Into<String>, agent_name: impl Into<String>) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Completed,
|
||||
current_tool: None,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this agent as failed with an error message.
|
||||
pub fn failed(
|
||||
agent_id: impl Into<String>,
|
||||
agent_name: impl Into<String>,
|
||||
error: String,
|
||||
) -> Self {
|
||||
AgentProgress {
|
||||
agent_id: agent_id.into(),
|
||||
agent_name: agent_name.into(),
|
||||
status: AgentStatus::Failed(error),
|
||||
current_tool: None,
|
||||
steps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! System prompts and directive templates for agent and subagent turns.
|
||||
//!
|
||||
//! Centralising all prompt text here keeps the core turn logic free of
|
||||
//! hardcoded prose, making prompts easier to maintain, review, and localise.
|
||||
//!
|
||||
//! # Flow
|
||||
//! The application layer's `AgentTurnServiceImpl` calls `main_agent_prompt()`
|
||||
//! to construct the system message at the start of each turn. Subagent and
|
||||
//! review prompts are provided by their respective modules.
|
||||
|
||||
/// Build the main-agent system prompt.
|
||||
///
|
||||
/// The prompt establishes the agent's identity as Zesdex, an AI coding
|
||||
/// assistant, and defines the priority hierarchy that governs tool selection:
|
||||
///
|
||||
/// 1. **Workflow first** — `workflow_run` / `hive_mind` for complex tasks
|
||||
/// 2. **Planning & TODOs** — `plan_enter` / `todowrite` for structural work
|
||||
/// 3. **Reasoning** — `seq_think` for deep analysis
|
||||
/// 4. **Tool execution** — direct tools for simple actions
|
||||
pub fn main_agent_prompt() -> String {
|
||||
"\
|
||||
You are Zesdex, an AI coding assistant. You have access to various tools \
|
||||
via native function calling to help the user.
|
||||
|
||||
CRITICAL DIRECTIVES & PRIORITY HIERARCHY:
|
||||
1. WORKFLOW FIRST: For any multi-step, complex, or non-trivial task, \
|
||||
you MUST prioritise using `workflow_run` (to construct and execute a \
|
||||
multi-phase YAML workflow) or `hive_mind` (to orchestrate parallel \
|
||||
autonomous agents). Workflows are your primary strategy.
|
||||
2. PLANNING & TODOS: Use `plan_enter` to establish high-level \
|
||||
architectural plans and `todowrite` to maintain granular task checklists.
|
||||
3. REASONING: Use `seq_think` for deep step-by-step analysis.
|
||||
4. TOOL EXECUTION: Execute individual tools (file edits, terminal commands) \
|
||||
within or guided by your workflows. If an error occurs, analyse and fix it.
|
||||
|
||||
Respond conversationally, concisely, and helpfully."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Build a subagent directive prompt.
|
||||
///
|
||||
/// The directive is embedded in a system message that also communicates the
|
||||
/// current working directory and workspace root so the subagent can resolve
|
||||
/// paths correctly.
|
||||
pub fn subagent_directive(directive: &str, cwd: &str, ws_root: &str) -> String {
|
||||
format!(
|
||||
"\
|
||||
You are a focused subagent.
|
||||
|
||||
Current directory (PWD): {cwd}
|
||||
Workspace root: {ws_root}
|
||||
|
||||
Your directive:
|
||||
{directive}
|
||||
|
||||
Complete the directive autonomously using the tools available to you. \
|
||||
Return your final answer when done."
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a conversation-compaction prompt.
|
||||
///
|
||||
/// The LLM is asked to produce a concise bulleted summary of the key
|
||||
/// requests, decisions, tools executed, and files modified.
|
||||
pub fn compaction_prompt() -> String {
|
||||
"\
|
||||
You are a helpful assistant summarising conversation history. \
|
||||
Provide a concise summary of the key user requests, decisions, tools \
|
||||
executed, and modified files. Format as a clear bulleted list."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn main_prompt_is_non_empty() {
|
||||
let prompt = main_agent_prompt();
|
||||
assert!(!prompt.is_empty());
|
||||
assert!(prompt.contains("Zesdex"));
|
||||
assert!(prompt.contains("WORKFLOW FIRST"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_directive_includes_directive_text() {
|
||||
let prompt = subagent_directive("test directive", "/home", "/home/project");
|
||||
assert!(prompt.contains("test directive"));
|
||||
assert!(prompt.contains("/home"));
|
||||
assert!(prompt.contains("/home/project"));
|
||||
}
|
||||
}
|
||||
@@ -104,36 +104,52 @@ impl SessionLock {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
|
||||
/// Check whether a process with the given PID is currently alive.
|
||||
/// Check whether a process with the given PID is currently alive and
|
||||
/// belongs to the same binary (mitigating PID-reuse races).
|
||||
///
|
||||
/// Strategy (Unix):
|
||||
/// 1. Resolve `/proc/<pid>/exe` — if it doesn't match our own binary,
|
||||
/// the PID either belongs to another process or is reused — return false.
|
||||
/// 2. Send `kill(pid, 0)` to verify the process is still alive.
|
||||
/// 3. Re-check `/proc/<pid>/exe` to close the TOCTOU window between
|
||||
/// step 1 and step 2 (PID reuse after exe check, before kill).
|
||||
///
|
||||
/// Uses `kill(pid, 0)` on Unix via the `nix` or `libc` crate in production;
|
||||
/// here we provide a best-effort check using the process table.
|
||||
/// On non-Unix platforms this always returns `true` (conservative).
|
||||
fn is_alive(pid: u32) -> bool {
|
||||
// On Unix, signal 0 checks process existence without sending a signal.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks
|
||||
// whether the process exists and the caller has permission to signal it.
|
||||
// The integer argument is a PID validated by `try_lock`.
|
||||
// Resolve our own executable path once.
|
||||
let self_exe = match std::fs::read_link("/proc/self/exe") {
|
||||
Ok(exe) => exe,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let pid_signed: i32 = match pid.try_into() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
|
||||
|
||||
// Phase 1: read /proc/<pid>/exe and compare with self_exe.
|
||||
let target = match std::fs::read_link(&proc_exe) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if target != self_exe {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 2: verify the process is still alive.
|
||||
// SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks
|
||||
// whether the process exists and the caller has permission to signal it.
|
||||
if unsafe { libc::kill(pid_signed, 0) != 0 } {
|
||||
return false;
|
||||
}
|
||||
// Extra check: verify the PID belongs to a zesdex process via
|
||||
// /proc/<pid>/exe to mitigate the PID-reuse race.
|
||||
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
|
||||
if let Ok(target) = std::fs::read_link(&proc_exe) {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if target != exe {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
|
||||
// Phase 3: re-check /proc/<pid>/exe to detect PID reuse between
|
||||
// Phase 1 and Phase 2.
|
||||
matches!(std::fs::read_link(&proc_exe), Ok(recheck) if recheck == self_exe)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
//! - `ChatMessage` — a single message with role, content, and tool metadata
|
||||
//! - `Role` — message role enum (User, Assistant, System, Tool)
|
||||
|
||||
pub use crate::core::message::{ChatMessage, Role};
|
||||
pub use crate::core::conversation::Conversation;
|
||||
pub use crate::core::message::{ChatMessage, Role};
|
||||
@@ -14,6 +14,8 @@
|
||||
//! 3. Oldest entries are evicted from the in-memory cache when
|
||||
//! `MAX_MEMORY_ENTRIES` is exceeded (prevents unbounded growth)
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single recorded file edit event.
|
||||
@@ -47,18 +49,18 @@ pub const MAX_MEMORY_ENTRIES: usize = 10_000;
|
||||
|
||||
/// In-memory view of a session's edit log.
|
||||
///
|
||||
/// Wraps a `Vec<EditLogEntry>` and provides basic query helpers.
|
||||
/// Wraps a `VecDeque<EditLogEntry>` and provides basic query helpers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditLog {
|
||||
/// Ordered list of edit entries (newest appended last).
|
||||
pub entries: Vec<EditLogEntry>,
|
||||
pub entries: VecDeque<EditLogEntry>,
|
||||
}
|
||||
|
||||
impl EditLog {
|
||||
/// Create an empty edit log with no entries.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
entries: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ pub mod settings;
|
||||
pub use app_config::AppConfig;
|
||||
pub use app_config::ModelRole;
|
||||
pub use app_config::ProviderConfig;
|
||||
pub use commands::{NewMemory, SettingsPatch};
|
||||
pub use conversation::Conversation;
|
||||
pub use edit_log::EditLog;
|
||||
pub use edit_log::EditLogEntry;
|
||||
@@ -46,7 +47,6 @@ pub use repository::SettingsRepository;
|
||||
pub use service::ConversationService;
|
||||
pub use service::MemoryService;
|
||||
pub use service::SettingsService;
|
||||
pub use commands::{NewMemory, SettingsPatch};
|
||||
pub use settings::InternetMode;
|
||||
pub use settings::Settings;
|
||||
pub use settings::SettingsFlags;
|
||||
@@ -56,11 +56,7 @@ pub trait ConversationRepository {
|
||||
fn load(&self, session_dir: &Path) -> Result<Conversation, RepositoryError>;
|
||||
|
||||
/// Persist a `Conversation` to the given session directory.
|
||||
fn save(
|
||||
&self,
|
||||
session_dir: &Path,
|
||||
conversation: &Conversation,
|
||||
) -> Result<(), RepositoryError>;
|
||||
fn save(&self, session_dir: &Path, conversation: &Conversation) -> Result<(), RepositoryError>;
|
||||
}
|
||||
|
||||
/// Persistence contract for `Memory` (long-term agent memory entries).
|
||||
|
||||
@@ -44,11 +44,7 @@ pub trait ConversationService {
|
||||
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError>;
|
||||
|
||||
/// Append a single `ChatMessage` to the conversation and persist.
|
||||
fn add_message(
|
||||
&self,
|
||||
conv: &mut Conversation,
|
||||
msg: ChatMessage,
|
||||
) -> Result<(), ServiceError>;
|
||||
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError>;
|
||||
}
|
||||
|
||||
/// Use-cases for long-term memory management.
|
||||
|
||||
@@ -218,14 +218,27 @@ impl SseParser {
|
||||
///
|
||||
/// Return: all `StreamEvent`s completed by this chunk.
|
||||
pub fn feed(&mut self, chunk: &str) -> Vec<StreamEvent> {
|
||||
self.buffer.push_str(chunk);
|
||||
// Normalize \r\n and bare \r to \n for consistent line ending handling
|
||||
let chunk = chunk.replace("\r\n", "\n").replace('\r', "\n");
|
||||
|
||||
// Prevent unbounded buffer growth for long lines without \n
|
||||
const MAX_BUFFER_SIZE: usize = 1_048_576; // 1 MB
|
||||
if self.buffer.len() + chunk.len() > MAX_BUFFER_SIZE {
|
||||
tracing::warn!("SSE buffer exceeded maximum size, resetting");
|
||||
self.buffer.clear();
|
||||
self.event_type = None;
|
||||
self.data_lines.clear();
|
||||
}
|
||||
|
||||
self.buffer.push_str(&chunk);
|
||||
let mut events = Vec::new();
|
||||
while let Some(line_end) = self.buffer.find('\n') {
|
||||
let line = self.buffer[..line_end].trim_end_matches('\r').to_string();
|
||||
self.buffer = self.buffer[line_end + 1..].to_string();
|
||||
if line.is_empty() {
|
||||
events.extend(self.flush_event());
|
||||
} else if let Some(ty) = line.strip_prefix("event: ") {
|
||||
} else if let Some(ty) = line.strip_prefix("event:") {
|
||||
// Handle both "event:foo" and "event: foo"
|
||||
self.event_type = Some(ty.trim().to_string());
|
||||
} else if let Some(data) = line.strip_prefix("data:") {
|
||||
let data = data.trim_start().to_string();
|
||||
@@ -320,18 +333,22 @@ impl SseParser {
|
||||
if let Some(tool_calls) =
|
||||
d.get("tool_calls").and_then(|tc| tc.as_array())
|
||||
{
|
||||
const MAX_TOOL_CALLS: usize = 64;
|
||||
for tc in tool_calls {
|
||||
let index =
|
||||
tc.get("index").and_then(Value::as_u64).unwrap_or_else(
|
||||
|| {
|
||||
tracing::warn!(
|
||||
"[stream] tool call delta missing index, \
|
||||
let raw_index = tc
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"[stream] tool call delta missing index, \
|
||||
defaulting to 0"
|
||||
);
|
||||
0
|
||||
},
|
||||
);
|
||||
let index = usize::try_from(index).unwrap_or(0);
|
||||
);
|
||||
0
|
||||
});
|
||||
// Clamp index to prevent out-of-bounds / memory exhaustion
|
||||
let index = usize::try_from(raw_index)
|
||||
.unwrap_or(0)
|
||||
.min(MAX_TOOL_CALLS.saturating_sub(1));
|
||||
let id = tc
|
||||
.get("id")
|
||||
.and_then(|i| i.as_str())
|
||||
@@ -372,6 +389,19 @@ impl SseParser {
|
||||
}
|
||||
d_events
|
||||
}
|
||||
"content_block_delta" => {
|
||||
let mut d_events = Vec::new();
|
||||
if let Some(delta) = value.get("delta") {
|
||||
if let Some(content) = delta.get("text").and_then(|c| c.as_str()) {
|
||||
d_events.push(StreamEvent::Token(content.to_string()));
|
||||
}
|
||||
if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str())
|
||||
{
|
||||
d_events.push(StreamEvent::Reasoning(reasoning.to_string()));
|
||||
}
|
||||
}
|
||||
d_events
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ impl Store {
|
||||
///
|
||||
/// Why: paths are computed, not created — call `ensure_dirs` before use.
|
||||
pub fn new() -> Self {
|
||||
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok()
|
||||
.or_else(|| std::env::var("HOME").ok().map(|h| format!("{h}/.local/share")))
|
||||
{
|
||||
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok().or_else(|| {
|
||||
std::env::var("HOME")
|
||||
.ok()
|
||||
.map(|h| format!("{h}/.local/share"))
|
||||
}) {
|
||||
PathBuf::from(data_dir).join("zesdex")
|
||||
} else {
|
||||
PathBuf::from(".local/share/zesdex")
|
||||
|
||||
+23
-12
@@ -24,29 +24,40 @@
|
||||
//! no framework imports, no side effects. All persistence is expressed
|
||||
//! through repository traits that infrastructure adapters implement.
|
||||
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cms;
|
||||
pub mod core;
|
||||
pub mod error;
|
||||
pub mod subagent;
|
||||
pub mod workflow;
|
||||
|
||||
// Re-export all public items from each module for ergonomic imports.
|
||||
// Consumers can do `use zesdex_domain::*` for common types.
|
||||
pub use auth::{
|
||||
IamSession, NewSession, OAuthConfig, OAuthToken, OAuthRepository, OAuthService,
|
||||
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session,
|
||||
SessionId, SessionLock, SessionLockRepository, SessionRepository, SessionService,
|
||||
IamSession, NewSession, OAuthConfig, OAuthRepository, OAuthService, OAuthToken,
|
||||
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, SessionId,
|
||||
SessionLock, SessionLockRepository, SessionRepository, SessionService,
|
||||
};
|
||||
pub use cms::{
|
||||
AppConfig, AppConfigRepository, Conversation as CmsConversation,
|
||||
ConversationRepository, ConversationService, EditLog, EditLogEntry,
|
||||
EditLogRepository, InternetMode, Memory, MemoryRepository, MemoryService,
|
||||
ModelRole, NewMemory, ProviderConfig, RepositoryError as CmsRepositoryError,
|
||||
ServiceError as CmsServiceError, Settings, SettingsFlags, SettingsPatch,
|
||||
SettingsRepository, SettingsService,
|
||||
AppConfig, AppConfigRepository, Conversation as CmsConversation, ConversationRepository,
|
||||
ConversationService, EditLog, EditLogEntry, EditLogRepository, InternetMode, Memory,
|
||||
MemoryRepository, MemoryService, ModelRole, NewMemory, ProviderConfig,
|
||||
RepositoryError as CmsRepositoryError, ServiceError as CmsServiceError, Settings,
|
||||
SettingsFlags, SettingsPatch, SettingsRepository, SettingsService,
|
||||
};
|
||||
pub use core::{
|
||||
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role,
|
||||
SseParser, StreamEvent, StreamOptions, Store, TokenUsage, ToolCall,
|
||||
ToolCallResult, ToolDef, ToolFunction, ToolFunctionDef, UsageStats,
|
||||
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, SseParser, Store,
|
||||
StreamEvent, StreamOptions, TokenUsage, ToolCall, ToolCallResult, ToolDef, ToolFunction,
|
||||
ToolFunctionDef, UsageStats,
|
||||
};
|
||||
pub use error::DomainError;
|
||||
|
||||
// Agent module top-level items (TurnEvent, SessionRuntime, etc.)
|
||||
pub use agent::*;
|
||||
// Sub-module items need explicit re-exports
|
||||
pub use agent::defaults::*;
|
||||
pub use agent::progress::AgentProgress;
|
||||
pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive};
|
||||
pub use subagent::*;
|
||||
pub use workflow::*;
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Subagent domain models.
|
||||
|
||||
/// Access tier for subagent tool permissions.
|
||||
///
|
||||
/// Tiers are cumulative: `Write` includes everything in `Read`, and `Full`
|
||||
/// includes everything in `Write`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccessTier {
|
||||
/// Read-only: search, read, glob, utility tools (no mutations).
|
||||
Read,
|
||||
/// Read + Write: above plus write, edit, delete, git, memory.
|
||||
Write,
|
||||
/// Full: above plus bash, shell, LSP, workflow, plan tools.
|
||||
Full,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//! Workflow and Hive-mind domain models.
|
||||
|
||||
/// A single phase in a parsed workflow script.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkflowPhase {
|
||||
pub name: String,
|
||||
pub directive: String,
|
||||
}
|
||||
|
||||
/// A parsed workflow script with named phases.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkflowScript {
|
||||
pub name: String,
|
||||
pub phases: Vec<WorkflowPhase>,
|
||||
}
|
||||
|
||||
/// A directive for a single processing node in the hive mind.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeDirective {
|
||||
pub directive: String,
|
||||
pub access_tier: String,
|
||||
}
|
||||
|
||||
/// A cognitive cycle plan — ordered list of cycles, each containing
|
||||
/// parallel node directives.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CognitiveCyclePlan {
|
||||
pub cycles: Vec<Vec<NodeDirective>>,
|
||||
}
|
||||
|
||||
/// A single cycle in a cognitive cycle plan — parallel node directives
|
||||
/// executed together.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CognitiveCycle {
|
||||
pub index: u32,
|
||||
pub directives: Vec<NodeDirective>,
|
||||
}
|
||||
|
||||
/// Output from a single hive-mind processing node after a cycle completes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeOutput {
|
||||
pub id: String,
|
||||
pub directive: String,
|
||||
pub output: String,
|
||||
}
|
||||
@@ -42,3 +42,8 @@ dirs.workspace = true
|
||||
rusqlite.workspace = true
|
||||
axum.workspace = true
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
[[bin]]
|
||||
name = "test_load"
|
||||
path = "src/bin/test_load.rs"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
//! configuration files plus a seed session for development/testing.
|
||||
//! Invoked as `cargo run --bin seed`.
|
||||
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let store = zesdex_domain::core::Store::new();
|
||||
store.ensure_dirs()?;
|
||||
@@ -46,13 +45,11 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
// Create a seed session
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
let session = zesdex_domain::auth::Session::new(
|
||||
session_id.clone(),
|
||||
"Seed Session".to_string(),
|
||||
);
|
||||
let session = zesdex_domain::auth::Session::new(session_id.clone(), "Seed Session".to_string());
|
||||
// Persist via the session repository
|
||||
use zesdex_domain::SessionRepository;
|
||||
let repo = zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
|
||||
let repo =
|
||||
zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
|
||||
repo.save_session(&store.base_dir, &session)?;
|
||||
tracing::info!("Seed session created: id={session_id}");
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use zesdex_domain::cms::AppConfigRepository;
|
||||
use zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository;
|
||||
|
||||
fn main() {
|
||||
let base_dir = dirs::home_dir().unwrap().join(".local/share/zesdex");
|
||||
let repo = JsonAppConfigRepository::new();
|
||||
let config = repo.load(&base_dir).unwrap();
|
||||
|
||||
println!("Providers:");
|
||||
for (k, v) in &config.providers {
|
||||
println!(" - {} (default model: {:?})", k, v.default_model);
|
||||
}
|
||||
|
||||
println!("Default provider: {}", config.default_provider);
|
||||
println!("Default model: {}", config.default_model);
|
||||
println!("Model roles:");
|
||||
for (k, v) in &config.model_roles {
|
||||
println!(" - {}: provider={}, model={}", k, v.provider, v.model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ClaudeEnv {
|
||||
#[serde(alias = "ANTHROPIC_BASE_URL")]
|
||||
anthropic_base_url: Option<String>,
|
||||
#[serde(alias = "ANTHROPIC_API_KEY")]
|
||||
anthropic_api_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ClaudeSettings {
|
||||
env: Option<ClaudeEnv>,
|
||||
#[serde(alias = "customModel", alias = "model")]
|
||||
custom_model: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = dirs::home_dir()
|
||||
.unwrap()
|
||||
.join(".claude")
|
||||
.join("settings.json");
|
||||
println!("Path: {:?}", path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
println!("File content length: {}", content.len());
|
||||
match serde_json::from_str::<ClaudeSettings>(&content) {
|
||||
Ok(settings) => {
|
||||
println!("Parsed successfully: {:?}", settings);
|
||||
if let Some(env) = settings.env {
|
||||
println!("Base URL: {:?}", env.anthropic_base_url);
|
||||
println!("API Key: {:?}", env.anthropic_api_key);
|
||||
} else {
|
||||
println!("No env block");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Parse error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Read error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@ use clap::Parser;
|
||||
|
||||
/// Zesdex — autonomous AI coding agent.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "zesdex", version, about = "Autonomous AI coding agent with TUI")]
|
||||
#[command(
|
||||
name = "zesdex",
|
||||
version,
|
||||
about = "Autonomous AI coding agent with TUI"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Run as background daemon with IPC socket
|
||||
#[arg(long)]
|
||||
@@ -136,12 +140,23 @@ fn run_api_server(port: u16) -> anyhow::Result<()> {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
rt.block_on(async {
|
||||
let store = zesdex_domain::core::Store::new();
|
||||
store.ensure_dirs()?;
|
||||
|
||||
// Load JWT secret from environment variable with a secure default warning
|
||||
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
|
||||
tracing::warn!(
|
||||
"JWT_SECRET environment variable not set; using insecure default. \
|
||||
Set JWT_SECRET to a secure random value in production."
|
||||
);
|
||||
zesdex_domain::agent::defaults::FALLBACK_JWT_SECRET.to_string()
|
||||
});
|
||||
|
||||
let state = zesdex_api::ApiState::new(
|
||||
store.base_dir.clone(),
|
||||
"dev-secret",
|
||||
jwt_secret,
|
||||
"",
|
||||
"deepseek-v4-flash-free",
|
||||
Some("https://opencode.ai/zen/v1".to_string()),
|
||||
zesdex_domain::agent::defaults::DEFAULT_MODEL,
|
||||
Some(zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()),
|
||||
);
|
||||
let app = zesdex_api::build_router(state);
|
||||
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
---
|
||||
name: clean-code
|
||||
description: Apply Robert C. Martin's (Uncle Bob's) Clean Code, Clean Architecture, and Clean Craftsmanship principles when writing, reviewing, or refactoring code. Use this skill whenever the user asks to write new code of non-trivial size (functions, classes, modules, services), refactor or clean up existing code, review code for quality, design a module or system boundary, write tests, or whenever the user mentions "clean code," "clean architecture," "SOLID," "SRP," "OCP," "LSP," "ISP," "DIPLine truncated
|
||||
---
|
||||
|
||||
# Clean Code (Uncle Bob)
|
||||
|
||||
This skill codifies the principles that Robert C. Martin teaches across *Clean Code* (2008), *Clean Architecture* (2017), *Clean Craftsmanship* (2021), his blog at blog.cleancoder.com, the older wiki at butunclebob.com, his Google Sites articles, and his courses at cleancoder.com. It applies during code generation, review, refactoring, and system design.
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
Three mental anchors to carry into every edit:
|
||||
|
||||
1. **Code is read far more than written.** The ratio is well over 10:1. Optimize for the reader — future teammates, and your future self.
|
||||
2. **The Boy Scout Rule.** Leave every module cleaner than you found it — even if just by renaming one variable or extracting one tiny function.
|
||||
3. **The only way to go fast is to go well.** Dirty code slows the whole team down. "We'll clean it up later" rarely happens, and productivity collapses into the Productivity Roller-Coaster or the Grand-Redesign Myth. Clean as you go, always.
|
||||
|
||||
Some quotes from other practitioners Uncle Bob cites:
|
||||
|
||||
- "Clean code is simple and direct. Clean code reads like well-written prose." — Grady Booch
|
||||
- "Clean code always looks like it was written by someone who cares." — Michael Feathers
|
||||
- "You know you are working on clean code when each routine you read turns out to be pretty much what you expected." — Ward Cunningham
|
||||
|
||||
## How to Use This Skill in a Session
|
||||
|
||||
When generating, reviewing, or refactoring code, work in this order:
|
||||
|
||||
1. **Name first.** Before writing a function body, confirm the name tells you what it does and why. If you cannot name it, you do not yet understand it.
|
||||
2. **Extract till you drop.** "A function does one thing if, and only if, you cannot extract another function from it." Keep extracting until you cannot.
|
||||
3. **Keep diffs honest.** When refactoring, do not also change behavior. When adding a feature, refactor *before* or *after*, never *during*.
|
||||
4. **Prefer tests first** for non-trivial logic. If infeasible, write them immediately after. See [references/tdd.md](references/tdd.md).
|
||||
5. **Respect boundaries.** Business rules must never depend on frameworks, databases, or UIs. See [references/architecture.md](references/architecture.md).
|
||||
6. **Prefer polymorphism to conditionals** for anything that varies by type — place if/else/switch in a factory that creates polymorphic objects. See [references/paradigms.md](references/paradigms.md).
|
||||
7. **Reread as a stranger.** Before declaring a task done, reread the code as if you had not written it.
|
||||
|
||||
## Deeper References
|
||||
|
||||
When the task calls for it, load the matching reference file:
|
||||
|
||||
- **[references/solid.md](references/solid.md)** — The five SOLID principles (SRP, OCP, LSP, ISP, DIP), their origins in Parnas (1972), Meyer (1988), Liskov (1987), their 2020 re-affirmation, and the component principles (REP, CCP, CRP, ADP, SDP, SAP). Load when designing a class, module, or microservice boundary.
|
||||
- **[references/architecture.md](references/architecture.md)** — Clean Architecture: the Dependency Rule, concentric layers, Screaming Architecture, why "the database is a detail," Ivar Jacobson's use-case foundation. Load when structuring a new service, deciding what a microservice should own, or untangling framework coupling.
|
||||
- **[references/tdd.md](references/tdd.md)** — The Three Laws of TDD, F.I.R.S.T., canonical test taxonomy (unit/acceptance/integration/system/micro/functional), test doubles, Chicago vs. London schools, fragile tests, the Transformation Priority Premise, the Cycles of TDD. Load when writing or reviewing tests.
|
||||
- **[references/paradigms.md](references/paradigms.md)** — The three programming paradigms (structured, OO, functional), the reductionist definitions Uncle Bob uses for each, the Data/Object Anti-Symmetry (why DTOs are not objects), polymorphism as the heart of OO, if-else-switch refactoring, why FP and OO are orthogonal not exclusive. Load when the task involves choosing between procedural and OO style, writing code in a functional language, refactoring switch statements, or handling persistLine truncated
|
||||
- **[references/craft.md](references/craft.md)** — The craftsmanship ethic: mess vs. technical debt, Martin's First Law of Documentation, Saying No, estimation, pairing guidelines, "Going Fast" vs. "Speed Kills," the "Screaming Architecture" mindset applied to whole projects. Load when the task raises professional-judgment questions.
|
||||
- **[references/oath.md](references/oath.md)** — The Programmer's Oath. Load when the task raises a question of professional responsibility (shipping under pressure, accumulating "temporary" hacks, padding estimates, degrading code to hit a deadline).
|
||||
|
||||
The remaining sections are the core rulebook for code at the function and class level. Scan them against anything you produce.
|
||||
|
||||
---
|
||||
|
||||
## 1. Meaningful Names
|
||||
|
||||
Names are the single highest-leverage lever for readability. From Tim Ottinger's naming rules, expanded in the book:
|
||||
|
||||
- **Use intention-revealing names.** `int d;` → `int elapsedTimeInDays;`. Names should answer: *What is this? Why does it exist? How is it used?*
|
||||
- **Avoid disinformation.** Don't call something `accountList` unless it truly is a List. Don't use lowercase `l` or uppercase `O` as variable names (look like 1 and 0).
|
||||
- **Make meaningful distinctions.** `productInfo` vs. `productData` is noise. `a1`, `a2`, `a3` is a red flag.
|
||||
- **Use pronounceable, searchable names.** `genymdhms` is bad. Single letters are acceptable only for tiny local scopes.
|
||||
- **Class names are nouns** (`Customer`, `WikiPage`, `AddressParser`). **Method names are verbs** (`postPayment`, `deletePage`, `save`).
|
||||
- **Do not encode types.** Skip Hungarian notation, `m_` prefixes, `I` prefixes for interfaces.
|
||||
- **Ubiquitous language.** Use the business domain's vocabulary. If the domain says "policyholder," do not name it `user`.
|
||||
- **Pick one word per concept.** Standardize `fetch` vs. `retrieve` vs. `get`. Same for `controller` vs. `manager` vs. `driver`.
|
||||
- **Add meaningful context, and no more.** Scattered `firstName`/`street`/`city` need an `addr_` prefix or an `Address` type.
|
||||
|
||||
## 2. Functions
|
||||
|
||||
> First rule: functions should be small. Second rule: smaller than that.
|
||||
|
||||
- **Target ~20 lines, often far fewer.** If you cannot see the whole function without scrolling, it is too long.
|
||||
- **Do one thing.** *Operational definition:* a function does one thing if, and only if, you cannot extract another function from it (Uncle Bob, "Extract till you drop," 2009).
|
||||
- **One level of abstraction per function.** The Step-Down Rule: public high-level functions at top, calling slightly lower-level helpers, and so on. Reading top to bottom should feel like descending a staircase.
|
||||
- **Extract till you drop.** Most programmers stop far too early. Extract until you cannot.
|
||||
- **Descriptive names beat short names.** A long descriptive name is better than a long descriptive comment.
|
||||
- **Few arguments.** 0 ideal. 1–2 fine. 3 suspect. 4+ almost always means a struct or a split.
|
||||
- **No flag arguments.** `render(true)` is terrible. Split into `renderForSuite()` and `renderForSingleTest()`.
|
||||
- **No hidden side effects.** A function named `checkPassword` must not also initialize a session.
|
||||
- **Command-Query Separation.** A function either *does* something or *answers* something, never both.
|
||||
- **Tell, don't ask.** Alan Kay's original OO concept: cells in a biological system tell each other what to do; they do not ask for state and decide. Neurons, hormones — all tellers.
|
||||
- **Prefer exceptions (or Result types) to error codes.**
|
||||
- **Extract try/catch bodies.** Each should be a single function call.
|
||||
- **Avoid switch statements** in business logic. See [references/paradigms.md](references/paradigms.md) for the factory+polymorphism pattern.
|
||||
- **Don't Repeat Yourself.** Duplication is the root of many evils.
|
||||
|
||||
**Example — doing one thing:**
|
||||
|
||||
Bad:
|
||||
```
|
||||
def process_order(order):
|
||||
if not order.items:
|
||||
raise ValueError("Empty")
|
||||
total = sum(i.price * i.qty for i in order.items)
|
||||
total *= 1.1 # tax
|
||||
send_email(order.customer, f"Total: {total}")
|
||||
db.save(order, total)
|
||||
return total
|
||||
```
|
||||
|
||||
Clean:
|
||||
```
|
||||
def process_order(order):
|
||||
validate(order)
|
||||
total = calculate_total_with_tax(order)
|
||||
notify_customer(order, total)
|
||||
persist(order, total)
|
||||
return total
|
||||
```
|
||||
|
||||
## 3. Comments
|
||||
|
||||
> "Don't comment bad code — rewrite it." — Brian Kernighan
|
||||
|
||||
Every comment represents a failure to make the code self-explanatory. Before writing a comment, ask: *can I rename or extract to make this unnecessary?*
|
||||
|
||||
**Good comments (rare but valuable):**
|
||||
- Legal headers (when required).
|
||||
- Informative comments you cannot encode in names (regex explanations, format specs, wire protocol details).
|
||||
- Explanation of **intent** — *why*, not *what*. Why this ordering, why this tradeoff, why this workaround.
|
||||
- Clarification of obscure arguments or return values you cannot rename.
|
||||
- Warnings of consequences ("this test takes two hours to run").
|
||||
- TODOs — prune regularly; a stale TODO list is worse than none.
|
||||
- Public API documentation (Javadoc, rustdoc, TSDoc, etc.).
|
||||
- **Genuine complexity that resists expression in code.** Uncle Bob's own "Necessary Comments" (2017) example: a "choked function" (throttled cache wrapper) required a timing diagram in the test comments because no amount of naming or extraction could communicate the six interleaved test cases. Rare — but it happens.
|
||||
|
||||
**Bad comments — delete on sight:**
|
||||
- Mumbling (written for yourself, unclear to others).
|
||||
- Redundant (restates the code).
|
||||
- Misleading (out of date or wrong — actively harmful).
|
||||
- Mandated (comment-every-function policies produce noise).
|
||||
- Journal/changelog (that's what git is for).
|
||||
- Noise (`// default constructor`).
|
||||
- Commented-out code (delete it; git remembers).
|
||||
- Closing-brace comments (`} // end if`) — your function is too long.
|
||||
- Attributions (`// added by Rick`) — `git blame` exists.
|
||||
|
||||
**Martin's First Law of Documentation** (from *Agile Software Development: PPP*): Produce no document unless its need is immediate and significant. Note the scope: *documents*, not code comments. Agile is not the rejection of documentation — that is "flawed religious behavior." Documentation earns its keep on a prioritized, ROI basis.
|
||||
|
||||
## 4. Formatting
|
||||
|
||||
**Vertical formatting — the newspaper metaphor:**
|
||||
- Top of file: high-level concept. Details grow as you scroll.
|
||||
- Related concepts stay vertically close.
|
||||
- Dependent functions: caller above callee (the Step-Down Rule).
|
||||
- Blank lines separate concepts, not pad.
|
||||
|
||||
**Horizontal formatting:**
|
||||
- Keep lines readable (~100–120 chars).
|
||||
- Do not align assignments artificially.
|
||||
- Use indentation consistently; never collapse a multi-branch `if`/`while` onto one line.
|
||||
- Follow the team's standard. Consistency within a project beats any individual preference.
|
||||
|
||||
**Indentation is an abstraction-level signal.** Ideal functions have zero indentation beyond the function body; one or two levels (one if/while, one try) is acceptable. Deeper nesting usually means you have missed an extraction.
|
||||
|
||||
## 5. Objects and Data Structures
|
||||
|
||||
**Data/Object Anti-Symmetry** (Chapter 6, *Clean Code*):
|
||||
|
||||
- **Object:** a set of functions that operate on **implied** data. Data exists but is hidden.
|
||||
- **Data structure:** a set of data elements operated on by **implied** functions. Data is exposed; functions are not specified by the structure itself.
|
||||
|
||||
These are **complements**, not siblings. Consequences:
|
||||
|
||||
- DTOs are data structures, not objects.
|
||||
- Database tables are data structures, not objects.
|
||||
- "ORM" is a misnomer — there is no real mapping between tables and objects.
|
||||
- **The axis of expected change determines the style.** If you expect more new functions than new types, prefer procedural style (data structures + functions). If you expect more new types than new functions, prefer OO (classes + polymorphism). The Visitor pattern bridges the two.
|
||||
|
||||
Other rules:
|
||||
|
||||
- **Law of Demeter — don't talk to strangers.** A method `f` of class `C` should only call methods of: `C` itself, objects it creates, objects passed as arguments, or objects it holds as fields. Avoid train wrecks: `a.getB().getC().doSomething()`.
|
||||
- **Tell, don't ask.** Instead of asking for state and deciding, tell the object to do the work.
|
||||
|
||||
More on this in [references/paradigms.md](references/paradigms.md).
|
||||
|
||||
## 6. Error Handling
|
||||
|
||||
- **Use exceptions (or Result types), not return codes.**
|
||||
- **Write try-catch-finally first** when an operation can fail. It defines the transactional scope.
|
||||
- **Provide context with exceptions.** Wrap third-party exceptions in your own types.
|
||||
- **Define exception classes by the needs of the caller**, not by implementation detail.
|
||||
- **Do not return null.** Return empty collections, use Option/Result/Maybe, or throw. Null checks pollute callers.
|
||||
- **Do not pass null.** If a function cannot handle null, do not accept it. Fail fast at the boundary.
|
||||
|
||||
## 7. Boundaries
|
||||
|
||||
- **Wrap third-party APIs in adapters.** Your code talks to the adapter, not the library. Localizes changes when the library upgrades or is replaced.
|
||||
- **Write learning tests** when exploring a new library: small, focused tests that probe its behavior. When the library upgrades, they tell you what broke.
|
||||
- Keep boundaries clean so replacing a dependency is a localized change, not a project-wide rewrite.
|
||||
- The big structural story is in [references/architecture.md](references/architecture.md).
|
||||
|
||||
## 8. Tests
|
||||
|
||||
See [references/tdd.md](references/tdd.md) for the full treatment. Essentials:
|
||||
|
||||
**Three Laws of TDD:**
|
||||
1. Do not write production code until you have a failing test.
|
||||
2. Do not write more of a test than is sufficient to fail.
|
||||
3. Do not write more production code than is sufficient to pass.
|
||||
|
||||
**F.I.R.S.T.:** Fast, Independent, Repeatable, Self-validating, Timely.
|
||||
|
||||
**Michael Feathers' definition of legacy code:** *Legacy code is code without tests.* Uncle Bob calls Feathers's *Working Effectively with Legacy Code* "the only book I know of that addresses this topic."
|
||||
|
||||
Test code is first-class. Hold it to the same clarity bar as production code.
|
||||
|
||||
## 9. Classes
|
||||
|
||||
- **Small.** For functions we counted lines. For classes we count **responsibilities**.
|
||||
- **Single Responsibility Principle.** The formulation has evolved: "do one thing" → "one reason to change" → most recently (Clean Architecture, 2017) "responsible to one, and only one, **actor**" (where actor is a person or tightly coupled group). See [references/solid.md](references/solid.md).
|
||||
- **Cohesion.** Methods should use most of the instance variables. Low cohesion = two classes in a trench coat.
|
||||
- **Organize for change.** Isolate volatile concepts behind interfaces so changes do not ripple.
|
||||
- **Class organization order:** public static constants → private static variables → private instance variables → public functions → private helpers (grouped with their public caller).
|
||||
|
||||
## 10. Systems
|
||||
|
||||
- **Separate construction from use.** Startup code (wiring dependencies) lives in one place; business logic does not touch it.
|
||||
- **Dependency injection** over hardcoded `new` expressions deep in business logic.
|
||||
- **Cross-cutting concerns** (logging, transactions, security, metrics) belong in middleware/aspects/interceptors, not scattered through the domain.
|
||||
- Let architecture **emerge** as the system grows, but defend the seams — the places where modules plug together — at every stage.
|
||||
- See [references/architecture.md](references/architecture.md).
|
||||
|
||||
## 11. Emergent Design — Kent Beck's Four Rules
|
||||
|
||||
A design is *simple* if, in priority order, it:
|
||||
|
||||
1. **Runs all the tests.**
|
||||
2. **Contains no duplication.**
|
||||
3. **Expresses the intent of the programmer.**
|
||||
4. **Minimizes the number of classes and methods.**
|
||||
|
||||
Order matters. Never sacrifice test coverage to reduce class count.
|
||||
|
||||
---
|
||||
|
||||
## 12. Code Smells — A Review Checklist
|
||||
|
||||
Scan for these before declaring code done.
|
||||
|
||||
**Function smells**
|
||||
- Too many arguments (>3).
|
||||
- Flag arguments (booleans that switch behavior).
|
||||
- Selector arguments (enums that drive internal switches).
|
||||
- Dead parameters, dead code paths.
|
||||
- Obscure intent — body reads as a sequence of mystery steps.
|
||||
- Misplaced responsibility — function lives on the wrong class/module.
|
||||
- Inappropriate static — method should be polymorphic.
|
||||
|
||||
**Class smells**
|
||||
- Feature envy (method uses another class's data more than its own).
|
||||
- Large class / god class.
|
||||
- Too many responsibilities.
|
||||
- Inappropriate intimacy between classes.
|
||||
- Lazy class — no longer pulls its weight.
|
||||
|
||||
**General smells**
|
||||
- **Duplication** — the #1 smell; hunt it everywhere.
|
||||
- Magic numbers or strings — extract named constants.
|
||||
- Inconsistent names for the same concept.
|
||||
- Artificial coupling — things glued together that don't belong.
|
||||
- Negative conditionals (`if (!isNotEmpty)`) — invert.
|
||||
- **If-else or switch chains on type** — replace with factory + polymorphism; see [references/paradigms.md](references/paradigms.md).
|
||||
- Dead code.
|
||||
- Vertical separation — variables declared far from use.
|
||||
- Boundaries violated — business logic importing framework classes, entities reaching the DB.
|
||||
|
||||
**Name smells**
|
||||
- Non-descriptive (`data`, `info`, `handle`, `process`).
|
||||
- Names not matching level of abstraction.
|
||||
- Mental mapping required (decode `r`, `q`, `tmp`).
|
||||
- Encoded names (Hungarian, type prefixes).
|
||||
- Side info stuffed into names ("the `u` here is because…").
|
||||
|
||||
**Test smells**
|
||||
- Insufficient tests.
|
||||
- Skipped or ignored tests accumulating.
|
||||
- Tests dependent on execution order.
|
||||
- Tests that test the framework, not your code.
|
||||
- Slow tests (they will stop being run).
|
||||
- Over-mocking — tests break on refactor without any real regression.
|
||||
|
||||
---
|
||||
|
||||
## A Note on Disagreement
|
||||
|
||||
Clean Code is not scripture. Uncle Bob himself has revised definitions across his career (SRP has three formulations; LSP was initially taught as about inheritance and later clarified as about subtyping). He has also publicly recommended John Ousterhout's *A Philosophy of Software Design* (2022) while noting disagreements with Ousterhout on two key Clean Code points:
|
||||
|
||||
- Ousterhout prefers larger functions with "deep implementations behind narrow interfaces."
|
||||
- Ousterhout advocates more use of comments.
|
||||
|
||||
These are honest, ongoing debates. The skill's default aligns with Uncle Bob; you are expected to think about the tradeoffs, not apply the rules blindly. When a codebase's structure makes the opposite choice more readable for the reader-in-context, the reader wins.
|
||||
|
||||
## One More Thing
|
||||
|
||||
Clean code is not a destination — it is a **practice**. Every function is an opportunity to practice. The single most important lesson is not any rule; it is the **attitude of caring enough to leave the code better than you found it**.
|
||||
|
||||
The professional commitment behind all of this is captured in [references/oath.md](references/oath.md) and elaborated in [references/craft.md](references/craft.md).
|
||||
@@ -0,0 +1,66 @@
|
||||
# Sources
|
||||
|
||||
This skill was built from material crawled from the Uncle Bob source network you provided. This file is an honest accounting of what was fetched and what wasn't, so you can verify the provenance of any claim and follow up if something seems off.
|
||||
|
||||
## Fully read (body captured)
|
||||
|
||||
### Course outlines (cleancoder.com/files/)
|
||||
|
||||
- cleanCodeCourse.md
|
||||
- cleanArchitectureCourse.md
|
||||
- tdd.md
|
||||
- advanced-tdd.md
|
||||
- clean-agile.md
|
||||
- immersion.md
|
||||
|
||||
### Blog posts (blog.cleancoder.com) — full or near-full body
|
||||
|
||||
- The Clean Architecture (2012-08-13)
|
||||
- The Programmer's Oath (2015-11-18)
|
||||
- The Single Responsibility Principle (2014-05-08, partial)
|
||||
- The Open Closed Principle (2014-05-12, via search)
|
||||
- OO vs FP (2014-11-24, via search)
|
||||
- First-Class Tests (2017-05-05, via search)
|
||||
- Testing Like the TSA (2017-03-06, via search)
|
||||
- Test Contra-variance (2017-10-03, via search)
|
||||
- Necessary Comments (2017-02-23)
|
||||
- Solid Relevance (2020-10-18)
|
||||
- if-else-switch (2021-03-06, via search)
|
||||
- Screaming Architecture (2011-09-30, via search)
|
||||
- A Little Architecture (2016-01-04, via search)
|
||||
- Classes vs Data Structures (2019-06-16, via search)
|
||||
- Functional Classes (2023-01-18, via search)
|
||||
- Loopy (2020-09-30, via search)
|
||||
|
||||
### Other sources
|
||||
|
||||
- cleancoder.com/books (full — Uncle Bob's recommended-reading list with annotations)
|
||||
- sites.google.com/site/unclebobconsultingllc/.../articles (index + inline excerpts of ~40 articles)
|
||||
- sites.google.com/.../articles/one-thing-extract-till-you-drop (full body after fighting Google Sites' massive-nav rendering)
|
||||
- butunclebob.com old-wiki pages via web_search:
|
||||
- ArticleS.UncleBob.PrinciplesOfOod (SRP evolution, component principles)
|
||||
- ArticleS.UncleBob.AgilePeopleStillDontGetIt (shipping untested code is unacceptable)
|
||||
- ArticleS.UncleBob.OnDocumentation (Martin's First Law of Documentation)
|
||||
- ArticleS.UncleBob.P2M2 (pairing guidelines)
|
||||
- ArticleS.UncleBob.IuseVisitor (Visitor pattern as SRP-preserver)
|
||||
- ArticleS.MichaelFeathers.LiskovSubstitutionInDynamicLanguages (LSP in dynamic languages)
|
||||
|
||||
## Titles only (index excerpts captured, body not fetched)
|
||||
|
||||
~120 additional blog.cleancoder.com posts — titles, dates, and (for ~30 of them) 1–3 sentence excerpts from Anthropic web_search results. Topics include: The Cycles of TDD, The Little Mocker, When to Mock, Monogamous TDD, Test Induced Design Damage?, The Transformation Priority Premise (+ three follow-ups), Three Paradigms, Why Clojure, FP Basics E1–E4, The Principles of Craftsmanship, The Humble Craftsman, Saying No, The Churn, The Lurn, NO DB, Clean Micro-service Architecture, FrameworkLine truncated
|
||||
|
||||
The remaining ~40 Google Sites top-level articles — again, titles captured with some inline excerpts, bodies not individually fetched due to Google Sites' nav-heavy rendering (each fetch costs ~20K tokens in nav alone before the article body begins).
|
||||
|
||||
## Not fetched
|
||||
|
||||
- butunclebob.com front page and the full wiki structure beyond the few articles surfaced via web_search. The old wiki is largely dormant; the direct URLs I tried returned empty pages; content was reachable only via search result excerpts.
|
||||
- cleancoders.com video-episode descriptions (peripheral; the book/blog material covers the same ground).
|
||||
- Uncle Bob's Twitter/X archive (referenced in a few places but I fetched only material that appeared in search results).
|
||||
|
||||
## What this means for the skill
|
||||
|
||||
- Core-principle claims (the SOLID wording, the Clean Architecture layers, the TDD three laws, the F.I.R.S.T. attributes, the test taxonomy, the Data/Object Anti-Symmetry, the three paradigms, the oath) are backed by directly fetched body text or search-excerpt evidence.
|
||||
- Some narrower historical and biographical claims (the Parnas 1972 citation, Liskov 1987 date, Meyer 1988 OOSC citation, attribution of specific phrasings to specific articles) were corroborated across multiple sources but not verified at their original citations. If any of them matters for a formal use, double-check against the original paper.
|
||||
- The sections on ~120 unfetched blog posts are not directly represented in the skill — the skill is built from the ~25 sources I did read fully, plus the consistent pattern of excerpts from the rest.
|
||||
|
||||
If there's a specific blog post from the unfetched list that you want me to integrate, point me at it and I'll fetch it directly and revise the relevant reference file.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Clean Architecture
|
||||
|
||||
When to load this reference: when structuring a new service or module, drawing boundaries between components, deciding what a microservice should own, untangling framework coupling, reviewing a system for testability and longevity, or choosing a top-level folder structure.
|
||||
|
||||
Clean Architecture is Uncle Bob's synthesis of Hexagonal Architecture (Alistair Cockburn), Onion Architecture (Jeffrey Palermo), DCI (Coplien & Reenskaug), and BCE (Ivar Jacobson, *Object-Oriented Software Engineering*, 1992). They differ in detail but agree on one goal: **separation of concerns by layering**, with business rules isolated from delivery mechanisms.
|
||||
|
||||
The foundational insight comes from Jacobson: **architectures are structures that support the use cases of the system.** Not frameworks. Not databases. Not UIs. Use cases.
|
||||
|
||||
---
|
||||
|
||||
## What a Clean Architecture Produces
|
||||
|
||||
A system that is:
|
||||
|
||||
1. **Independent of frameworks.** Frameworks are tools, not constraints.
|
||||
2. **Testable.** Business rules tested without UI, DB, web server, or any external element.
|
||||
3. **Independent of UI.** The UI can be replaced (web → console → CLI → TUI) without touching business rules.
|
||||
4. **Independent of database.** Swap PostgreSQL for MongoDB, ClickHouse, or in-memory without rewriting domain logic.
|
||||
5. **Independent of any external agency.** The core business rules know nothing about the outside world.
|
||||
|
||||
**The database is a detail.** So is the web. So is the framework. These are the most common sources of architectural rot because developers mistake them for foundations.
|
||||
|
||||
> "The database is merely an IO device. It happens to provide some useful tools for sorting, querying, and reporting but those are ancillary to the system architecture." — *A Little Architecture* (2016)
|
||||
|
||||
---
|
||||
|
||||
## The Dependency Rule
|
||||
|
||||
The one rule that makes everything else work:
|
||||
|
||||
> **Source code dependencies point only inward, toward higher-level policy.**
|
||||
|
||||
- Nothing in an inner layer may name anything from an outer layer — no function, class, variable, or data format.
|
||||
- Data formats convenient for the outer layer (ORM row struct, JSON DTO) must not leak inward.
|
||||
- Control flow may cross boundaries in either direction, but *source dependencies* point only inward. The Dependency Inversion Principle (see [solid.md](solid.md)) is the mechanism that makes this possible when control flow runs outward.
|
||||
|
||||
When this rule is obeyed, external details — databases, frameworks, UIs — become replaceable plugins.
|
||||
|
||||
---
|
||||
|
||||
## The Four Concentric Layers
|
||||
|
||||
Schematic. You may need more or fewer for a given system, but the Dependency Rule always applies.
|
||||
|
||||
### 1. Entities (innermost)
|
||||
|
||||
Encapsulate **enterprise-wide** business rules. An entity can be a class with methods or a data structure plus functions — style choice.
|
||||
|
||||
- Entities know nothing about applications, use cases, frameworks, or anything outside.
|
||||
- For single applications (no "enterprise"), these are your core business objects.
|
||||
- These are the least affected by operational change. Changes to page navigation, auth mechanisms, or DB schemas must not reach here.
|
||||
|
||||
### 2. Use Cases
|
||||
|
||||
Encapsulate **application-specific** business rules. Use cases orchestrate entities to accomplish the application's goals.
|
||||
|
||||
- A use case directs entities; it does not contain enterprise-wide rules itself.
|
||||
- Changes to the application's *behavior* land here. Changes to externalities do not.
|
||||
- Simple request/response data structures (not entities) flow in and out.
|
||||
|
||||
### 3. Interface Adapters
|
||||
|
||||
Convert data between the format convenient for use cases/entities and the format convenient for external agencies.
|
||||
|
||||
- MVC's Controllers, Presenters, and Views live here.
|
||||
- All SQL lives here (if the database is SQL). Nothing inside knows about SQL.
|
||||
- DTOs are translated into domain types and back here.
|
||||
|
||||
### 4. Frameworks and Drivers (outermost)
|
||||
|
||||
The web framework, the database, the message broker, the file system. Glue code only — you do not write much application logic here. Details live here because details change, and the outer ring is where change is cheap.
|
||||
|
||||
---
|
||||
|
||||
## Crossing Boundaries
|
||||
|
||||
When control flow needs to run outward — a use case needs to call a presenter — a direct call violates the Dependency Rule (the inner layer names something in the outer layer).
|
||||
|
||||
**Solution: the Dependency Inversion Principle.** The use case calls an interface (an "output port") defined in its own layer. The outer-layer presenter implements that interface. Control flows outward; source dependencies point inward. Same pattern works for repositories, gateways, any outward call.
|
||||
|
||||
---
|
||||
|
||||
## What Crosses Boundaries
|
||||
|
||||
Only **simple data structures** cross boundaries:
|
||||
- Plain structs or Data Transfer Objects.
|
||||
- Primitive arguments in function calls.
|
||||
- Maps/dictionaries, when appropriate.
|
||||
|
||||
Never pass Entity objects or ORM row objects across boundaries — that couples layers. Translate to the format most convenient for the inner circle at every boundary crossing.
|
||||
|
||||
---
|
||||
|
||||
## Screaming Architecture
|
||||
|
||||
From the 2011 blog post of the same name. The top-level layout of a project should *scream* what the system does, not what framework it uses.
|
||||
|
||||
**The blueprint metaphor.** Imagine looking at the blueprints of a building. A single-family residence: front entrance, foyer, living room, dining room, kitchen. A library: grand entrance, check-in clerks, reading areas, galleries of bookshelves. A shopping mall: corridors, store bays, parking lots. You can tell what kind of building it is before you see any sign.
|
||||
|
||||
What does *your* application architecture scream?
|
||||
|
||||
**Bad top-level:** `controllers/`, `models/`, `views/`, `services/`. Tells you the system uses MVC. Tells you nothing about what the system is for.
|
||||
|
||||
**Good top-level:** `billing/`, `shipping/`, `catalog/`, `fraud_detection/`. Now you know what the system does.
|
||||
|
||||
**Why it matters:** A good architecture lets you defer decisions about Rails, Spring, Hibernate, Tomcat, MySQL, or React until much later in the project. A framework-centric top-level locks those decisions in day one, and also makes the code base mute about its own purpose. The web is a *delivery mechanism*; the database is a *detail*. Neither should dominate your system structure.
|
||||
|
||||
If a stranger cannot tell from the directory structure whether they are looking at an e-commerce platform or a hospital records system, the architecture is failing at the highest level.
|
||||
|
||||
---
|
||||
|
||||
## Component Principles
|
||||
|
||||
Once modules are organized, they group into **components** — independently deployable units (libraries, services, jars, crates). Two sets of principles govern them.
|
||||
|
||||
### Component Cohesion
|
||||
|
||||
- **REP — Reuse/Release Equivalence Principle.** The unit of reuse is the unit of release.
|
||||
- **CCP — Common Closure Principle.** Group together classes that change for the same reasons at the same times. (SRP at component scale.)
|
||||
- **CRP — Common Reuse Principle.** Classes used together belong together; classes not used together don't. (ISP at component scale.)
|
||||
|
||||
These three pull in different directions — the **tension diagram** is a triangle and component design is an ongoing balance. Early-stage projects lean toward REP+CCP (ship quickly, include more); mature, widely-reused components shift toward CRP (exclude what clients don't need).
|
||||
|
||||
### Component Coupling
|
||||
|
||||
- **ADP — Acyclic Dependencies Principle.** The dependency graph among components must have no cycles. Break cycles with DIP or by extracting a new component both sides depend on.
|
||||
- **SDP — Stable Dependencies Principle.** Depend in the direction of stability.
|
||||
- **SAP — Stable Abstractions Principle.** Stable components should be abstract; volatile components should be concrete.
|
||||
|
||||
---
|
||||
|
||||
## Applying This in Practice
|
||||
|
||||
- **"NO DB" and "NO Web" are valid starting positions.** Business rules should be expressible, testable, and useful before either is chosen.
|
||||
- **Frameworks are tools, not partners.** Wrap them. Keep `import django` or `import axum::Router` out of the core. (Uncle Bob's 2014 "Framework Bound" is a full rant on this.)
|
||||
- **Not every project needs four full circles.** Small projects may collapse Entities and Use Cases into one layer. The Dependency Rule still applies whatever the count.
|
||||
- **The seams matter most.** Architecture lives at the boundaries between components. Defend them at every review — once they rot, replacing a dependency stops being a weekend task and becomes a six-month project.
|
||||
- **Dialog from *A Little Architecture* (2016).** An aspiring architect says they want to make decisions about databases, frameworks, and webservers. Uncle Bob's response: "Oh. Well, then you don't want to become a Software Architect after all." The architect's job is to make decisions that let you **defer** the irrelevant decisions.
|
||||
|
||||
---
|
||||
|
||||
## Architecture and Agility
|
||||
|
||||
From "The Scatology of Agile Architecture" (2009): Agile does *not* mean no up-front architecture. The myth that you evolve architecture from zero is, in Uncle Bob's words, "horse shit." Good teams do enough architecture up front to get the seams right, then let the details emerge inside those seams. See [craft.md](craft.md) for more on this.
|
||||
@@ -0,0 +1,135 @@
|
||||
# The Craftsmanship Ethic
|
||||
|
||||
When to load this reference: when the task raises questions of professional judgment — estimation, deadline pressure, sloppy code accumulating, pairing, saying no to bad requests, or when the user invokes "technical debt" or "mess" or "craftsmanship."
|
||||
|
||||
The behaviors in *Clean Code* and *Clean Architecture* are not ends in themselves. They are instrumental to a larger ethic that Uncle Bob has been refining since the early 2000s: the software craftsmanship movement, which evolved into the Programmer's Oath (see [oath.md](oath.md)) and the 2022 book *Clean Craftsmanship*. This reference captures the non-code parts of that ethic that still materially affect how Claude should behave when writing or reviewing code.
|
||||
|
||||
---
|
||||
|
||||
## Clean Code Is a Practice, Not a Destination
|
||||
|
||||
From many posts, consolidated:
|
||||
|
||||
- Every function is an opportunity to practice. You don't reach "clean" and stop.
|
||||
- The **Boy Scout Rule** is the daily discipline: leave each module cleaner than you found it, even if just by renaming one variable.
|
||||
- "The only way to go fast is to go well." Dirty code does not trade speed for quality; it trades illusory short-term speed for enormous long-term slowness. This is the Productivity Roller-Coaster: feel fast for weeks, slow to a crawl over months.
|
||||
- From *Going Fast*: "Fast" is a property you get by being disciplined, not by skipping discipline.
|
||||
- From *Speed Kills*: conversely, the illusion that you can get fast by cutting corners almost always kills a project.
|
||||
|
||||
---
|
||||
|
||||
## A Mess Is Not Technical Debt
|
||||
|
||||
**This distinction matters.** People conflate them, and the conflation is a way to make sloppiness sound respectable.
|
||||
|
||||
**Ward Cunningham's Technical Debt (the original, 1992):** a **deliberate, considered** engineering trade-off when a schedule or learning situation justifies using a suboptimal design temporarily. You know what the right design is; you are choosing the wrong one now, *with intent*, and you will fix it later. Example: initial website uses server-rendered pages because there's no time to build an Ajax framework.
|
||||
|
||||
**A Mess:** bad code written by someone who did not do the work to understand the problem, did not refactor, did not test, did not think. It is not "debt" because it was never a considered choice — it is just poor craftsmanship.
|
||||
|
||||
From "A Mess is not a Technical Debt" (2009): calling a mess "technical debt" launders bad craftsmanship as if it were responsible engineering. It is not. When refusing to ship a mess, do not accept the framing that "we're just taking on some debt." Debt is deliberate; a mess is sloppy.
|
||||
|
||||
**Fowler's four quadrants of debt** (prudent/imprudent × deliberate/inadvertent) are a better map:
|
||||
- Deliberate+prudent: the original Cunningham case ("we must ship now, we'll fix X next sprint").
|
||||
- Deliberate+imprudent: "we don't have time for design" (toxic, not actually debt).
|
||||
- Inadvertent+prudent: "now I know how we should have done it" (honest learning).
|
||||
- Inadvertent+imprudent: plain-old-mess masquerading as debt.
|
||||
|
||||
---
|
||||
|
||||
## Saying No
|
||||
|
||||
From "Saying No!" (2009) and elaborated in *The Clean Coder*: professionals have an obligation to refuse impossible or unethical demands.
|
||||
|
||||
- When a manager asks for something that cannot be done correctly in the time allowed, the professional answer is "no, but here's what I can do," not "yes" followed by silent quality compromise.
|
||||
- "Yes and then failing to deliver" is worse than "no" — the manager loses the ability to plan around reality.
|
||||
- Professionals push back on their own estimates. If pressure makes you shorten a number you believe, you have stopped being the expert the organization pays you to be.
|
||||
|
||||
Applied to Claude: when a user asks for something that cannot be done well under the stated constraints (skip the tests, skip the error handling, ship something that will crash), the right response includes the pushback. Offer what you *can* deliver cleanly, not a degraded version of what was asked for.
|
||||
|
||||
---
|
||||
|
||||
## Honest Estimates
|
||||
|
||||
From "Why is Estimating so Hard?" (2012) and related posts:
|
||||
|
||||
- Estimates are **probability distributions, not numbers.** Give a range: optimistic, nominal, pessimistic. Three-point estimates are honest; single-point estimates almost always compress uncertainty.
|
||||
- "I don't know yet, let me do a spike" is a professional answer. "I'll have it by Friday" said under duress without real confidence is not.
|
||||
- An estimate is not a commitment; commitments come from negotiating after estimates are honestly given.
|
||||
|
||||
---
|
||||
|
||||
## On Documentation
|
||||
|
||||
**Martin's First Law of Documentation** (from *Agile Software Development: PPP*): "Produce no document unless its need is immediate and significant."
|
||||
|
||||
This is often misread as "Agile means no documentation." It does not. From the butunclebob.com wiki:
|
||||
|
||||
> "Agile Development is NOT development without documentation. Rejecting documentation in the name of 'Agility' is a flawed religious behavior. It is just as flawed as uncritically accepting the production of dozens of different documents."
|
||||
|
||||
Documentation, like any engineering activity, is prioritized by ROI. Create documents that more than pay back the effort to produce them. Skip documents written because policy requires them but no one will read them.
|
||||
|
||||
What counts as documentation:
|
||||
- API docs (rustdoc, TSDoc, javadoc) — high value, close to code.
|
||||
- Architecture decision records (ADRs) — capture *why* decisions were made.
|
||||
- Onboarding / how-to guides — pay back every time a new person joins.
|
||||
- Specs for important flows — pay back every time a flow breaks.
|
||||
|
||||
What does not:
|
||||
- Status reports that recapitulate information already in the tracker.
|
||||
- Design documents written after implementation that no one will read.
|
||||
- Comments that restate the code.
|
||||
|
||||
---
|
||||
|
||||
## Pairing Guidelines
|
||||
|
||||
From "Pairing Guidelines" (2021) and earlier posts:
|
||||
|
||||
- Pairing is a **tool**, not a religion. Use it when it works; don't when it doesn't.
|
||||
- Mature agile teams pair maybe 50–70% of the time, not 100%.
|
||||
- Some problems require "time, focus, and silence" to study before attacking. Pairing on those is worse than solo.
|
||||
- Pair at the start of a story to align direction; solo for deep-focus passages; reunite to review.
|
||||
- The strategy "separate the syntax issues from the semantic issues" is a useful pattern when stuck as a pair — refactor the mechanical noise (parsing, config, regex) into a helper module so the core algorithm can be reasoned about on its own.
|
||||
|
||||
---
|
||||
|
||||
## Shipping Under Pressure
|
||||
|
||||
From "AgilePeopleStillDontGetIt" (2006) and "We must ship now and deal with consequences" (2009):
|
||||
|
||||
- "It is completely unacceptable to release code that you aren't sure works. Either make sure it works, or don't ship it. Period."
|
||||
- "A feature that crashes is much worse than a feature that doesn't exist. A feature that doesn't exist will defer revenue. A feature that crashes makes enemies out of customers."
|
||||
- "Our customers interpret features as promises. When we release a feature we are promising that it works. When it crashes we have broken that promise."
|
||||
- "Shipping untested software is shipping something unfinished and your customers will force you to finish it. The pressure will be higher at orders of magnitude if you finish it AFTER you have shipped it."
|
||||
|
||||
Applied to Claude: when asked to ship quickly and drop tests, the honest response is that the tests aren't slowing you down; they are the only way to ship correctly. "Going fast" without tests produces code that will return tenfold in debugging and firefighting over the next weeks.
|
||||
|
||||
---
|
||||
|
||||
## Professionalism Is Not Rigid Formalism
|
||||
|
||||
From "Why the sea is boiling hot" (2009) — the closing statement of Uncle Bob's 2009 Rails Conf keynote:
|
||||
|
||||
> "Professionalism does not mean rigid formalism. Professionalism does not mean adhering to bureaucracy. Professionalism is **honor**. Professionalism is being honest with yourself and disciplined in the way you work. Professionalism is not letting fear take over."
|
||||
|
||||
Honor and discipline. Not process for its own sake. The rules in this skill are tools for being disciplined; they are not a rulebook to hide behind.
|
||||
|
||||
---
|
||||
|
||||
## The Tricky Bit
|
||||
|
||||
From "The Tricky Bit" (2010): a British MP flew the Concorde and complained to the designer that going supersonic "didn't feel any different at all." The designer beamed: "Yes, that was the tricky bit."
|
||||
|
||||
Clean code, good architecture, solid tests — when they are working, the reader doesn't notice. The absence of friction is the product. Code that *announces* how clever it is, how much architecture it has, how sophisticated its patterns are, is usually the opposite of clean. The goal is invisibility — the reader moves through the code and feels nothing but understanding.
|
||||
|
||||
---
|
||||
|
||||
## When Claude Should Invoke Any of This
|
||||
|
||||
- **User wants to skip tests "just this once":** reference the "A Mess is not Debt" framing and the shipping-under-pressure material.
|
||||
- **User wants a speculative number instead of a range:** offer a range and explain why.
|
||||
- **User wants you to document something they won't read:** suggest the minimum viable doc that pays its way.
|
||||
- **User wants a "quick fix" that you can see will rot the module:** explain the Boy Scout Rule cost — a quick fix that makes the code worse is a negative-value change even at zero time cost.
|
||||
- **User says "we're doing Agile, we don't write documentation":** redirect to Martin's First Law and the "it's about ROI" framing.
|
||||
|
||||
The oath ([oath.md](oath.md)) captures the promises. This file captures the attitude and the vocabulary for navigating the hard conversations where craft meets pressure.
|
||||
Loaded 50 of 250 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user