Compare commits

..
84 Commits
Author SHA1 Message Date
semantic-release-bot 5912a35bbb chore(release): 1.23.2 [skip ci]
## [1.23.2](https://github.com/asepharyana/zesdex/compare/v1.23.1...v1.23.2) (2026-09-03)

### Bug Fixes

* **tui:** sliding-window stream reconciler + UI polish ([d86d6ae](https://github.com/asepharyana/zesdex/commit/d86d6aeef2dcd4924ced14f21502a84f7e916a26))
2026-09-03 05:29:53 +00:00
zesdex d86d6aeef2 fix(tui): sliding-window stream reconciler + UI polish
- Replace naive reconcileStream with stateful sliding-window replay
  merger: tracks per-message replay cursors (_posContent/_posReasoning)
  to deduplicate the segment-replay waves 9router emits. Verified
  against 970 real captured chunks (100% clean output).
- TranscriptMessage: colored [TAG] headers, 4-space indented body,
  thin separator lines between messages, dimmed reasoning.
- InputView: divider above prompt, '❯' prompt, cursor block.
- 75 tests pass, typecheck clean, biome clean.
2026-09-03 12:28:56 +07:00
semantic-release-bot 30cc579773 chore(release): 1.23.1 [skip ci]
## [1.23.1](https://github.com/asepharyana/zesdex/compare/v1.23.0...v1.23.1) (2026-09-03)

### Bug Fixes

* **tui:** repair layout overflow + streaming dup; polish UI/state ([a54f164](https://github.com/asepharyana/zesdex/commit/a54f1644f5943399568194b4f129185a9d841f1d))
2026-09-03 04:01:28 +00:00
asepharyana a54f1644f5 fix(tui): repair layout overflow + streaming dup; polish UI/state
Two real bugs squashed:
- Layout: giving the header box and scrollbox an explicit width in a
  column-flex root made OpenTUI overflow vertically, pushing the input
  row and status bar entirely off-screen (only header+one message showed).
  Root cause confirmed in a minimal probe; fix = drop the redundant
  width so column children size off the root box.
- Streaming: 9router's OpenAI-compatible layer emits FULL-TEXT chunks
  (the whole transcript so far) instead of incremental deltas, so naive
  content+=token duplicated text ("Hello! 👋 How canHello! 👋 How can...").
  Added reconcileStream() that accepts both incremental and full-text
  modes (shorter-replay → ignore, growing-authoritative → adopt, else
  append), unit-tested for both.

UI/state polish:
- Rich transcript: per-role colour + labelled blocks (YOU/AI/SYS/TOOL),
  word-wrapped to terminal width, reasoning dimmed, no misleading
  "(empty)" on tool-calling assistant messages.
- Header: app · real model · session, spread across full width.
- Status bar: ● running/idle, live token usage (in/out), autocomplete
  hint, active toast.
- Bordered autocomplete dropdown + full-width help/overlay panels.
- Turn runner streams all TurnEvents into state (tool markers, agent
  progress, workflow updates, toasts, usage) — ui.tsx is now display-only.
2026-09-03 11:00:29 +07:00
semantic-release-bot 23e68dc979 chore(release): 1.23.0 [skip ci]
# [1.23.0](https://github.com/asepharyana/zesdex/compare/v1.22.0...v1.23.0) (2026-09-03)

### Bug Fixes

* **auth:** perbaiki base64url encode JWT — string di-base64 dulu sebelum replace chars ([1b130e6](https://github.com/asepharyana/zesdex/commit/1b130e69b36603f894bcd45469b9feef28b18562))
* **ci:** Release workflow install semantic-release tanpa merusak package.json ([8c99c6b](https://github.com/asepharyana/zesdex/commit/8c99c6b1ca94e1d2b80dd59bb96eb24b7b3b2dfa))

### Features

* **api:** port Fase 5 REST API — Bun.serve router + JWT middleware + auth/session/conversation/chat handlers + composition root ([49e7932](https://github.com/asepharyana/zesdex/commit/49e79322b4aa1824467e1c871cf68809c1a424ee))
* **auth:** port 3f auth infrastructure — Argon2 passwords + HS256 JWT tokens ([7e8a160](https://github.com/asepharyana/zesdex/commit/7e8a160b0e918d51dc559f55fa19395368a4d84e))
* **auth:** tambah OAuth loopback server untuk capture authorization-code redirect ([fd58b46](https://github.com/asepharyana/zesdex/commit/fd58b4661fd0928dd461c74a5cb7bc7870ca9692))
* **cli:** port Fase 4 CLI — parser arg, mode dispatch, single-process composition root + REPL loop, bootstrap seed ([c582dc1](https://github.com/asepharyana/zesdex/commit/c582dc1461024d5037b8351a0df6b9b11e967782))
* **cli:** wire mode dispatch ke server interfaces — --api/--ws/--grpc/--web start server; add tsconfig paths ([02a5523](https://github.com/asepharyana/zesdex/commit/02a55235cfadb3e5daf3774a04f6fc6abe5da78d))
* **daemon:** port Fase 5 daemon — IPC agent-driver loop (Submit/Close) + streaming tokens; wire CLI --daemon/--attach ([14d3556](https://github.com/asepharyana/zesdex/commit/14d3556179685587e62873f81a57125037ac5e5a))
* **infra:** port tool system ke TypeScript (37 tools + registry + executor) ([82b51f7](https://github.com/asepharyana/zesdex/commit/82b51f7921e33e912f1244152df509522e44cae3))
* **ipc:** port 3g IPC + bgbash — framed Unix-socket server/client + background job registry, wire bash tools ([4b4730c](https://github.com/asepharyana/zesdex/commit/4b4730cff7ee2a5c43f8906d17282d95de84cf7a))
* **rewrite:** bootstrap monorepo Bun + port domain layer ke TypeScript ([07e84e4](https://github.com/asepharyana/zesdex/commit/07e84e43b386388f75f8293879e49dff1db461ab))
* **rewrite:** tambah application layer — port traits & use cases TypeScript ([03538a4](https://github.com/asepharyana/zesdex/commit/03538a455a93ff554abb45a1e54809c9579763fd))
* **rewrite:** tambah infrastructure — LLM client + full file persistence ([a2c7e5d](https://github.com/asepharyana/zesdex/commit/a2c7e5d44c784508b4fe735ec3434df2407f76de))
* **subagent:** port 3c subagent engine — run_agent loop + spawn/delegate/parallel ([a14a227](https://github.com/asepharyana/zesdex/commit/a14a2275ff90059874166aec8c03bd44e6c7efee))
* **tui:** port Fase 4 TUI logic layer — InputState/Action dispatcher/key controller/slash commands + lightweight ANSI renderer + run loop; 20 test ([8098133](https://github.com/asepharyana/zesdex/commit/8098133ba50135c7a5f9a063221ffdb860e064cb))
* **web): port Fase 5 static file server w/ traversal protection; feat(grpc:** port health stub ([1a6268b](https://github.com/asepharyana/zesdex/commit/1a6268bde58c67b9d3cbabce08306d53f0b3791a))
* **workflow:** port 3d workflow + hive-mind engine — parse, execute, cycle, synthesis, docs ([edc0076](https://github.com/asepharyana/zesdex/commit/edc007619b63d0d91fdad79a6d534c112b8a3981))
* **ws:** port Fase 5 WS server — Bun native WebSocket, ZESDEX_WS_TOKEN guard, prompt turn proxy + streaming token/done/error ([d4d423a](https://github.com/asepharyana/zesdex/commit/d4d423a4cae62562da4ca4f0231f6a4f90481473))
2026-09-03 01:34:43 +00:00
asepharyana 8c99c6b1ca fix(ci): Release workflow install semantic-release tanpa merusak package.json
npm install -D menulis 5 paket semantic-release + 333 dep ke devDependencies
package.json — padahal repo pakai bun.lock. prepareCmd lalu jalankan
'bun install --frozen-lockfile' yang melihat package.json 'terpolusi'
(1561 paket vs 30 di bun.lock) -> 'lockfile had changes, but lockfile is
frozen' -> Release selalu gagal.

Fix: install semantic-release ke prefix terisolasi /tmp/sr-tools + tambah ke
GITHUB_PATH, jalankan 'semantic-release' langsung (bukan npx yang pakai
proses -D). prepareCmd (--frozen-lockfile) dibiarkan benar: bun.lock
version-agnostic, verifikasi lokal lolos.
2026-09-03 08:33:44 +07:00
asepharyana 9b28a829e5 chore: bersihkan flake.lock Rust/Nix mati — deploy infra 100% Bun
Todo Fase 6 final: hapus flake.lock (artefak era Rust/Nix, tidak ada flake.nix
lagi, deploy.yml sudah bun build --compile + scp binary). Verifikasi wire-shape
lama selesai: settings.json (api_keys/provider/model/review_*) diload true di
headless + TUI real agent. Gate: tsc 0 error, bun test 70 pass, TUI render.
2026-09-03 08:30:23 +07:00
asepharyana b5ebd07fa8 refactor: rombak arsitektur file ke modular clean-arch (vertikal per-feature)
Organisasi ulang layer-first (domain/application/infrastructure) menjadi
vertikal per-feature, behavior tidak berubah:

- src/shared/domain/  -> fondasi bersama (ex-domain/core): message, provider,
  tool_call, tool_result, store, error, usage, conversation
- src/features/agent/ -> LLM turn loop: domain (TurnEvent, AgentTurnParams) +
  application (turn_service, ports) + infrastructure (llm LlmClient + tools)
- src/features/cms/   -> settings/conversation/memory persistence: domain +
  application (services) + infrastructure (persistence repos)
- src/features/subagent/ -> orchestration: domain (AccessTier) + infrastructure
  (engine runner, context, provider, delegate, spawn_tools)
- src/features/workflow/ -> multi-phase + hive-mind: domain + infrastructure
- src/interfaces/     -> cli + tui (outer ring) tetap

Subagent-runner (engine.ts runAgent, division.ts toolsFor) dipindah ke
features/subagent (bukan agent tools) utk memutus coupling agent<->subagent.

Alias tsconfig: @zesdex/domain -> shared/domain, + @zesdex/agent,
@zesdex/agent-infra, @zesdex/cms, @zesdex/subagent, @zesdex/workflow, @zesdex/shared.
Bersihkan dead: application/ports PasswordService/TokenService/AuthService.
compose.ts (composition root) & bootstrap repoint ke barrel feature.

Gates: tsc --noEmit 0 error, bun test 70 pass/0 fail, bun build 147 modules,
./dist/zesdex --headless -> real turn OK.
2026-09-03 02:30:12 +07:00
asepharyana 7e6ed34f0d refactor: rombak jadi full CLI/TUI only — hapus semua interface server (api/ws/grpc/web/daemon) + wire TUI ke agent beneran
- Hapus total interfaces: api (REST), ws (WebSocket), grpc, web, daemon (IPC)
- Hapus infra server-only yang jadi dead code: infrastructure/ipc + seluruh
  stack auth (domain/auth, application/auth, persistence/iam, infrastructure/auth jwt/password/oauth_loopback)
- TUI (OpenTUI) jadi interface utama: wire ke runSingleProcess() real runtime,
  ganti mock '(no LLM configured)' dengan turnService.runTurn asli + streaming
  stream_token/reasoning/tool_result/system_note/error ke transcript
- Tampilkan model asli di header (mis. claude-opus-5), abort turn via Ctrl+C
- CLI jadi mode headless tambahan: zesdex --headless '<prompt>' one-shot turn
- Bersihkan package.json (script tui/headless), tsconfig paths, docker-compose,
  AGENTS.md; hapus StartOAuth/login stub
- verified: tsc 0 err, 70 test pass, build dist/zesdex; headless 'pong' real; TUI
  interactive render + real turn 'say pong in one word' -> assistant 'pong'
2026-09-03 01:19:27 +07:00
asepharyana 34322e492a chore: hapus infra Nix Rust mati; deploy.yml rombak ke Bun native
Semua flake/default/shell merujuk Cargo.lock + crate 'zesdex-gateway' yang
sudah dihapus saat rombak monorepo ke project tunggal Bun, sehingga
nix build .#default pasti gagal. Hapus infra Nix yang tak bisa dipakai:

- hapus flake.nix, default.nix, shell.nix, flakehub-publish-rolling.yaml
- deploy.yml: ganti nix build .#default -> setup-bun + bun build --compile
  + scp dist/zesdex -> /usr/local/bin/zesdex -> systemctl restart zesdex
- docker-compose.yml: RUST_LOG usang -> ZESDEX_API_PORT (env TS)

verify: tsc 0 err, bun test 79 pass, dist/zesdex build OK.
2026-09-03 00:14:22 +07:00
asepharyana 3e260e6077 refactor(tui): rombak renderer ke OpenTUI React bindings
Ganti renderer ANSI line-based (render.ts) dan event loop raw stdin dengan
OpenTUI React bindings (@opentui/react + @opentui/core, React 19.2):

- ui.tsx: komponen ZesdexApp (header/scrollbox transkrip/input/footer),
  adaptor toControllerKey (OpenTUI KeyEvent -> controller KeyEvent),
  bootTui (createCliRenderer + createRoot + mount).
- run.ts: pakai bootTui, jalankan OpenTUI sampai user quit.
- state/action/command/controller tetap logika murni (tidak tersentuh).
- 5 unit test baru untuk toControllerKey; 79 test total hijau.
- tsconfig: jsx react-jsx + jsxImportSource @opentui/react.
- hapus render.ts (tidak terpakai); hapus script format rusak (bun fmt).
- deps: @opentui/core, @opentui/react, react, @types/react.

verify: tsc 0 err, bun test 79 pass, dist/zesdex merender TUI via pty.
2026-09-03 00:10:01 +07:00
asepharyana b7006d1ad8 chore(release): selaraskan semantic-release ke Bun — bump package.json, build dist/zesdex asset; hapus release.config.cjs (konfig cargo lama) 2026-09-02 22:58:35 +07:00
asepharyana 0ee0cc4350 refactor: rombak dari monorepo ke project tunggal — flat src/{domain,application,infrastructure,interfaces}, satu package.json tanpa workspaces, tsconfig paths → src/*, update install.sh/Dockerfile/CI 2026-09-02 22:56:26 +07:00
asepharyana e91fda3691 chore: hapus sisa struktur Rust — 11 Cargo.toml, apps/infrastructure/skills, tree src kosong (apps/{domain,application,infrastructure,gateway,bootstrap}) 2026-09-02 22:51:10 +07:00
asepharyana e4e0379c45 docs: tandai Fase 6 selesai — status header Fase 0-6 semua done 2026-09-02 22:51:10 +07:00
asepharyana c165dff114 chore: hapus semua source Rust + Cargo.toml/lock + .cargo (247 file .rs, migrasi ke TS/Bun) 2026-09-02 22:51:10 +07:00
asepharyana 319198d2f0 docs: tandai Rust cleanup + lefthook removed di TODO 2026-09-02 22:50:38 +07:00
asepharyana f26e6ac9c6 chore: hapus lefthook.yml (git hooks Rust-only) 2026-09-02 22:50:38 +07:00
asepharyana f75c3860e9 docs: update TODO Fase 6 — install.sh/Dockerfile/ci/release sudah Bun; nix deploy dicomot 2026-09-02 22:50:38 +07:00
asepharyana 170b1d3d19 ci: migrasi CI/Dockerfile/install.sh ke Bun — setup-bun + bun install/check/test + --compile single binary; release gate ke bun run check 2026-09-02 22:50:38 +07:00
asepharyana 5d65c78fb4 docs: tandai Fase 4 TUI + bootstrap selesai di TODO (74 test) 2026-09-02 22:50:38 +07:00
asepharyana 8098133ba5 feat(tui): port Fase 4 TUI logic layer — InputState/Action dispatcher/key controller/slash commands + lightweight ANSI renderer + run loop; 20 test 2026-09-02 22:50:38 +07:00
asepharyana 2e81412605 docs: tandai Fase 5 lengkap (daemon selesai) — semua server interface {api,daemon,ws,grpc,web} + CLI dispatch 2026-09-02 22:50:38 +07:00
asepharyana 14d3556179 feat(daemon): port Fase 5 daemon — IPC agent-driver loop (Submit/Close) + streaming tokens; wire CLI --daemon/--attach 2026-09-02 22:50:38 +07:00
asepharyana d134678530 docs: tandai Fase 5 {ws,grpc,web} + CLI dispatch selesai; tersisa daemon 2026-09-02 22:50:38 +07:00
asepharyana 02a55235cf feat(cli): wire mode dispatch ke server interfaces — --api/--ws/--grpc/--web start server; add tsconfig paths 2026-09-02 22:50:38 +07:00
asepharyana 1a6268bde5 feat(web): port Fase 5 static file server w/ traversal protection; feat(grpc): port health stub 2026-09-02 22:50:38 +07:00
asepharyana 7030e26b3a docs: tandai Fase 5 WS selesai di TODO 2026-09-02 22:50:38 +07:00
asepharyana d4d423a4ca feat(ws): port Fase 5 WS server — Bun native WebSocket, ZESDEX_WS_TOKEN guard, prompt turn proxy + streaming token/done/error 2026-09-02 22:50:38 +07:00
asepharyana 3930d2e2c9 docs: tandai Fase 5 API selesai di TODO 2026-09-02 22:50:38 +07:00
asepharyana 49e79322b4 feat(api): port Fase 5 REST API — Bun.serve router + JWT middleware + auth/session/conversation/chat handlers + composition root 2026-09-02 22:50:38 +07:00
asepharyana 1b130e69b3 fix(auth): perbaiki base64url encode JWT — string di-base64 dulu sebelum replace chars 2026-09-02 22:50:38 +07:00
asepharyana 145184d091 chore: update bun.lock untuk workspace @zesdex/cli 2026-09-02 22:50:38 +07:00
asepharyana c582dc1461 feat(cli): port Fase 4 CLI — parser arg, mode dispatch, single-process composition root + REPL loop, bootstrap seed 2026-09-02 22:50:38 +07:00
asepharyana 4b4730cff7 feat(ipc): port 3g IPC + bgbash — framed Unix-socket server/client + background job registry, wire bash tools 2026-09-02 22:50:38 +07:00
asepharyana fd58b4661f feat(auth): tambah OAuth loopback server untuk capture authorization-code redirect 2026-09-02 22:50:38 +07:00
asepharyana 7e8a160b0e feat(auth): port 3f auth infrastructure — Argon2 passwords + HS256 JWT tokens 2026-09-02 22:50:38 +07:00
asepharyana edc007619b feat(workflow): port 3d workflow + hive-mind engine — parse, execute, cycle, synthesis, docs 2026-09-02 22:50:38 +07:00
asepharyana a14a2275ff feat(subagent): port 3c subagent engine — run_agent loop + spawn/delegate/parallel 2026-09-02 22:50:38 +07:00
asepharyana 82b51f7921 feat(infra): port tool system ke TypeScript (37 tools + registry + executor)
Fase 3b rewrite Zesdex dari Rust ke TypeScript/Bun:
- Tool interface, ToolCtx + ToolCtxBuilder, InfrastructureToolExecutor
- Registry: allTools (37 tool), toolDefs, toolIsRisky, toolIsParallelSafe
- FS tools: read, write, edit, delete
- Search: grep, glob (glob matcher ringan)
- Shell: bash (timeout + timeout_spawn), bash_output, bash_kill
- Git: git_operator, git_worktree, git_cred + checkGitDestructive, checkCredentialRead
- Memory: remember, forget, recall via MarkdownMemoryRepository
- Utilitas: cd, dir_list, dir_cache_update, pong, todowrite, todofinish
- Best-practice: best_practice, commit_convention + BestPracticeEngine
- Web search (async fetch ke SearXNG), semantic_search/rebuild_index/list_symbols
- Workflow/spawn/parallel_delegate (delegasi ke engine 3c/3d dengan placeholder)
- Graduated checks + resolve_path sandbox (anti path-escape)
- 16 test tool system (registry, guards, resolve_path, fs roundtrip)

54 test hijau (38 lama + 16 baru), tsc strict bersih.
2026-09-02 22:50:38 +07:00
asepharyanaandClaude Opus 5 a2c7e5d44c feat(rewrite): tambah infrastructure — LLM client + full file persistence
Sub-fase 3a & 3e infrastructure TypeScript (@zesdex/infrastructure):
- llm/provider.ts: LlmClient (OpenAI/Anthropic-compatible fetch + SSE,
  retry 10x exponential backoff + jitter, abort on 401/402/403,
  stream quota-aware tool-call delta accumulation)
- resolveApiKey: settings.api_keys → app_config.env_var → inline default
- utils: writeJsonAtomic (tmp+rename+fsync), slugify, writeOsc52,
  truncateChars, buildWorkspaceTree, runCommand
- persistence/cms: JsonSettingsRepository, JsonAppConfigRepository
  (Claude credential auto-detection dari ~/.claude/settings.json + env),
  JsonConversationRepository, JsonlEditLogRepository (append NDJSON),
  MarkdownMemoryRepository (YAML-ish frontmatter), FileRewindBlobRepository
- persistence/iam: FileSystemSessionRepository, FileSystemSessionLockRepository,
  FileSystemOAuthRepository
- Workspace @zesdex/infrastructure tsc clean, semua test domain+application
  tetap hijau (38 pass)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 22:50:38 +07:00
asepharyanaandClaude Opus 5 03538a455a feat(rewrite): tambah application layer — port traits & use cases TypeScript
Paket @zesdex/application (apps/packages/application):
- ports: ProviderService (chat/chatStream+abort), PasswordService,
  TokenService, AuthService
- agent: AgentTurnServiceImpl (loop 50 iterasi, auto-compact 60k chars,
  adaptive max-tokens 800/1600/4096, temp 0.2/0.7, ErrorTracker,
  eksekusi tool read-only paralel terbatas mempertahankan urutan)
  + compact_messages_with_ai
- auth: OAuthUseCase (PKCE S256 + CSRF state), SessionServiceImpl
- cms: ConversationServiceImpl, MemoryServiceImpl, SettingsServiceImpl
- 11 unit test Bun (PKCE, OAuth CSRF, turn_service helper)
- tsconfig paths untuk workspace @zesdex/*

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 22:50:38 +07:00
asepharyanaandClaude Opus 5 07e84e43b3 feat(rewrite): bootstrap monorepo Bun + port domain layer ke TypeScript
Mulai migrasi Zesdex dari Rust ke TypeScript/Bun.

- Root workspace package.json, tsconfig strict, bun.lock, gitignore fix
- Paket @zesdex/domain (apps/packages/domain):
  - core: ChatMessage/Conversation/ToolCall/SseParser/repairJson/
    sanitizeToolArguments/UsageStats/Store/DomainError
  - auth: Session/SessionId/SessionLock/OAuth + repository & service interfaces
  - cms: AppConfig/Settings/SettingsPatch/Memory/EditLog + repository &
    service interfaces
  - agent: TurnEvent/SessionRuntime/AgentTurnParams/AgentProgress/prompt
  - subagent & workflow models
- Wire-shape JSON disamakan dengan Rust (serde rename/flatten/skip_if)
- 27 unit test Bun port dari test Rust (SseParser, repair_json, resolve_effective_model, session id)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 22:50:38 +07:00
semantic-release-bot 9d1544d799 chore(release): 1.22.0 [skip ci]
# [1.22.0](https://github.com/asepharyana/zesdex/compare/v1.21.2...v1.22.0) (2026-08-28)

### Features

* **agent:** wire auto-review (review_enabled no-op -> nyata) ([a27e815](https://github.com/asepharyana/zesdex/commit/a27e8151b39da4c8759e8e922ef132212327e413))
2026-08-28 08:31:47 +00:00
asepharyana a27e8151b3 feat(agent): wire auto-review (review_enabled no-op -> nyata)
review_enabled (default TRUE) selama ini no-op: tampil di TUI settings
overlay ("Review: true"), bisa di-toggle via command, TAPI spawn_background_review
tidak pernah dipanggil & Origin::Reviewer tidak pernah dikonstruksi. User
melihat "Review: true" padahal auto-review setelah edit tak pernah jalan.

Sekarang feature yang sudah dibangun penuh (subagent/auto/engine.rs: git diff
-> LLM review -> auto-fix HIGH/MEDIUM) di-wire:
- daemon/handler.rs: trigger setelah run_turn bila review_enabled; capture
  flag+creds SEBELUM api_key/provider_cfg di-move ke LlmClient.
- tui/turn.rs: sama, gated by state.settings.flags.review_enabled.
- ws/lib.rs: channel minimal tanpa settings -> review nyala tiap prompt
  (konsisten dgn default ON).

Aman: review fire-and-forget (tokio::spawn), get_git_diff skip bila no-change,
no-op bila bukan git repo (auto/engine). Creds dipakai = creds ter-resolve yg
sama dgn komposisi turn.

Verifikasi: check/clippy/fmt/test workspace hijau (0 error/warning/fail).
2026-08-28 15:27:33 +07:00
semantic-release-bot 88c2a7d000 chore(release): 1.21.2 [skip ci]
## [1.21.2](https://github.com/asepharyana/zesdex/compare/v1.21.1...v1.21.2) (2026-08-28)

### Performance Improvements

* **agent:** symbol index tak pegang mutex global saat rebuild I/O ([3b25e38](https://github.com/asepharyana/zesdex/commit/3b25e3898f9ba2fc1c58b991ad95a8c0bbe98601))
2026-08-28 07:58:48 +00:00
asepharyana 3b25e3898f perf(agent): symbol index tak pegang mutex global saat rebuild I/O
Sebelumnya SemanticSearch/ListSymbols/RebuildIndex menahan SYMBOL_INDEX
Mutex selama full `rebuild` (walk seluruh workspace, bisa detikan) +
selama search. Di main loop yang menjalankan read-only tools paralel,
semantic_search/list_symbols lain jadi BLOCK selama rebuild.

Refactor:
- Global berubah Mutex<Option<SymbolIndex>> -> OnceLock<Mutex<HashMap<
  workspace, SymbolIndex>>> — index per-workspace, jadi pencarian workspace B
  tidak mungkin bocor simbol stale dari A (workspace-awareness kini struktural,
  bukan hanya via needs_rebuild).
- ensure_symbol_index(workspace, force): rebuild dijalankan DI LUAR lock
  (mutex hanya dicek/insert/lookup singkat), lalu hasilnya di-swap-in di bawah
  short lock. Search/list/rebuild-report tak lagi memblock thread lain selama
  walk I/O. Per-workspace key menghilangkan race lintas-workspace dari skema
  swap tunggal.
- Test +1 (test_ensure_symbol_index_per_workspace_isolation): verifikasi dua
  workspace punya index independen, rebuild A tidak menimpa B.

Verifikasi: check/clippy -D warnings/fmt clean; test infra 64 (0 gagal).
2026-08-28 14:54:54 +07:00
semantic-release-bot d23d3855ec chore(release): 1.21.1 [skip ci]
## [1.21.1](https://github.com/asepharyana/zesdex/compare/v1.21.0...v1.21.1) (2026-08-28)

### Bug Fixes

* **agent:** semantic_search symbol index workspace-aware ([104af3a](https://github.com/asepharyana/zesdex/commit/104af3abb9e626c5d00ea87d523248d606d523ee))
2026-08-28 07:23:23 +00:00
asepharyana 104af3abb9 fix(agent): semantic_search symbol index workspace-aware
SymbolIndex global sudah melacak workspace_path tapi SemanticSearch dan
ListSymbols Cuma rebuild saat index kosong (is_empty). Akibat: setelah
mengindeks workspace A, mencari di workspace B diam-diam mengembalikan
simbol STALE dari A — menyesatkan coding agent (referensikan simbol yang
tidak ada di repo aktif).

Fix:
- Tambah SymbolIndex::needs_rebuild(workspace) — true bila index kosong
  ATAU workspace diminta beda dari yang ter-cache.
- Pakai di 2 call site (SemanticSearch::run, ListSymbols::run) menggantikan
  is_empty(), jadi pindah workspace otomatis trigger rebuild.
- test: +1 (test_needs_rebuild_workspace_aware — verifikasi flip workspace
  memicu rebuild bolak-balik A -> B -> A).

Catatan (bukan bug, dilaporkan): mutex SYMBOL_INDEX masih dipegang selama
full rebuild di run() — bottleneck saat semantic_search dipanggil paralel;
perbaikan butuh restrukturisasi double-checked rebuild, tak diubah di sini.

Verifikasi: check/clippy -D warnings/fmt clean; test infra 63 (0 gagal).
2026-08-28 14:19:22 +07:00
semantic-release-bot 62fa85867c chore(release): 1.21.0 [skip ci]
# [1.21.0](https://github.com/asepharyana/zesdex/compare/v1.20.2...v1.21.0) (2026-08-28)

### Features

* **agent:** hive-mind consensus synthesis pakai LLM nyata ([f75ff74](https://github.com/asepharyana/zesdex/commit/f75ff740ac2fa63340656e1e8215440f5e48063d))
2026-08-28 07:18:51 +00:00
asepharyana f75ff740ac feat(agent): hive-mind consensus synthesis pakai LLM nyata
synth_consensus selama ini Cuma concatenate output node lalu dilabeli
"Consensus" — tidak ada sintesis. Kini:

- Resolve kredensial LLM (provider/model/base_url/api_key) dari Store,
  sumber yang sama dgn execute_cycle.
- Kirim prompt sintesis ke model: minta distilasi node outputs jadi satu
  laporan konsensus berisi AGREEMENTS / CONFLICTS / KEY FINDINGS /
  RECOMMENDATION.
- Graceful fallback ke summary concatenation bila panggilan LLM gagal /
  output kosong, supaya sintesis konsensus tidak pernah merusak siklus
  hive-mind (konsisten dgn filosofi isolated-errors utk node).
- Batasi output per-node (MAX_NODE_OUTPUT_CHARS=4000, char-safe via
  truncate_chars) agar prompt tetap bounded.
- test: +2 (truncation char-safe pada output besar multi-byte; concat
  summary memuat semua node id).

Verifikasi: cargo check/clippy -D warnings/fmt clean; test infra 62 (0
gagal). Disk root sudah di-cargo clean (free 43.7GB, turun 98% -> 63%).
2026-08-28 14:15:03 +07:00
semantic-release-bot e349a35716 chore(release): 1.20.2 [skip ci]
## [1.20.2](https://github.com/asepharyana/zesdex/compare/v1.20.1...v1.20.2) (2026-08-28)

### Bug Fixes

* **agent:** recall search + memory_dir fallback + bersihkan dead llm_client ([f1f58b9](https://github.com/asepharyana/zesdex/commit/f1f58b9996f8eb58871d44fdd41c2d863874f253))
2026-08-28 05:57:17 +00:00
asepharyana f1f58b9996 fix(agent): recall search + memory_dir fallback + bersihkan dead llm_client
Hasil audit round 4 (workflow/hive_mind + memory + semantic_search).

- fix(memory): recall.search selama ini TIDAK pernah dipakai — tool
  mengiklankan keyword search di skema tapi run() cuma list semua nama.
  Kini search benar-benar memfilter (cocok di name/description/content,
  case-insensitive), + output 'No memories match' bila kosong.
- fix(memory): ToolCtxBuilder tidak punya setter memory_dir dan tak ada
  call-site yang mengisinya — remember/recall/forget memakai PathBuf kosong
  dan menulis memory ke CWD (bukan lokasi persisten). Tambah setter
  memory_dir + worktrees_dir, dan helper resolve_memory_dir() yang fallback
  ke Store::new().memory_dir bila ctx.memory_dir kosong; dipakai di ketiga
  tool memory.
- refactor(workflow): hapus LlmClient dummy di WorkflowRun (dibuat dengan
  API key kosong + model default + base_url default lalu tak pernah dipakai
  — execute_workflow menerimanya sebagai _llm_client). Kini execute_workflow
  tak ambil parameter tak terpakai; LLM asli tetap lewat execute_primitive
  yang resolve kredensial dengan benar.
- test: +2 (recall search memfilter; resolve_memory_dir fallback/eksplisit).

Catatan audit yang dilaporkan (belum difix): synth_consensus hanya
menggabungkan output (label Consensus menyesatkan, bukan sintesis LLM), dan
semantic_search memegang Mutex index global saat full rebuild (bottleneck
saat paralel) + index tidak workspace-aware.

PENTING (infra): disk root 100% saat kerja. Saya bebaskan ~4.6G dari /tmp +
cache aman (sekai*, verify-z, bun/npm cache). target/debug di repo = 38G —
rampah, perlu cargo clean + rebuild (jangan dibiarkan).
2026-08-28 12:53:32 +07:00
semantic-release-bot f4c02fd64e chore(release): 1.20.1 [skip ci]
## [1.20.1](https://github.com/asepharyana/zesdex/compare/v1.20.0...v1.20.1) (2026-08-28)

### Bug Fixes

* **agent:** subagent patuhi tool-calling contract + truncation char-safe ([e982cbe](https://github.com/asepharyana/zesdex/commit/e982cbeb041baea9cd500e2a29862a7eca9e6e17))
2026-08-28 04:47:39 +00:00
asepharyana e982cbeb04 fix(agent): subagent patuhi tool-calling contract + truncation char-safe
Hasil audit alur AI agent round 3 (fokus correctness & latent crash).

- fix(subagent): engine.rs sebelumnya mengeksekusi tool lalu push
  ChatMessage::tool hasil TANPA mendahuluinya dengan pesan assistant yang
  mendeklarasikan tool_calls → history malformed ([..., tool, tool,
  assistant(text)]). Kontrak OpenAI/Anthropic mensyaratkan pesan assistant
  (berisi tool_calls) sebelum hasil tool. Kini push response_msg
  (assistant + tool_calls + content) sebelum eksekusi, dan hapus push
  assistant content-only di akhir (agar tidak duplikat). Loop utama sudah
  benar; subagent kini selaras.
- fix(utils): &content[..1500] / &content[..1000] di build_rich_context
  dan &diff[..5000] di auto/engine.rs bisa panic saat indeks byte jatuh di
  tengah karakter multi-byte UTF-8 (emoji/CJK/panah). Tambah helper
  truncate_chars() yang memotong per karakter (char-safe) dan pakai di
  3 titik tersebut.
- test: +4 unit test truncate_chars (ASCII, potong, multibyte no-panic,
  emoji).

Catatan audit: subagent/auto (auto-review) & build_rich_context adalah dead
code (spawn_background_review & build_rich_context tidak pernah dipanggil).
Auto-review jangan diaktifkan asal (parser format teks rapuh + tanpa
verifikasi pasca-fix) — dilaporkan, bukan dicolokkan.
2026-08-28 11:43:42 +07:00
semantic-release-bot 20ce81a6be chore(release): 1.20.0 [skip ci]
# [1.20.0](https://github.com/asepharyana/zesdex/compare/v1.19.6...v1.20.0) (2026-08-28)

### Features

* **agent:** subagent tool paralel + auto-load AGENTS.md + verify cek setelah edit ([21e3ccc](https://github.com/asepharyana/zesdex/commit/21e3ccc891ab886044a5f0a770db710769d1287b))
2026-08-28 02:54:09 +00:00
asepharyana 21e3ccc891 feat(agent): subagent tool paralel + auto-load AGENTS.md + verify cek setelah edit
Lanjutan audit alur AI agent (round 2), mengisi celah yang tersisa dari
perpbaikan paralel tool di loop utama (74b1ad4) agar lebih mirip Claude Code.

- feat(subagent): eksekusi batch tool read-only paralel di subagent engine
  (engine.rs). Tool::run sinkron, jadi pakai scoped OS thread (bounded
  window 8); hasil dipertahankan dalam urutan panggilan asli. Batch dengan
  tool mutating jatuh balik ke jalur sequential aman.
- feat(agent): auto-load AGENTS.md/CLAUDE.md/.cursorrules ke system prompt
  tiap turn (seperti Claude Code load AGENTS.md saat startup). Fungsi
  main_agent_prompt_with_project_context menempel blok PROJECT CONTEXT;
  dibaca dari workspace root pertama & dibatasi 12k char.
- feat(prompt): arahan VERIFY AFTER EDIT — setelah edit/write, agent wajib
  jalankan cargo check/clippy/test (atau lint/test sesuai stack) via bash
  sebelum mengakhiri turn; perbaiki error yang terlihat, jangan klaim
  'compiles/works' tanpa hasil nyata.
- feat(infra): build_rich_context kini membaca AGENTS.md & CLAUDE.md juga
  (untuk explore_codebase/scout).
- test: +3 subagent engine (order paralel, kecepatan konkuren, fallback
  mutating), +2 domain prompt (konteks proyek & fallback kosong).
2026-08-28 09:50:21 +07:00
semantic-release-bot 992e60980c chore(release): 1.19.6 [skip ci]
## [1.19.6](https://github.com/asepharyana/zesdex/compare/v1.19.5...v1.19.6) (2026-08-27)

### Performance Improvements

* **agent:** eksekusi tool read-only paralel seperti Claude Code ([74b1ad4](https://github.com/asepharyana/zesdex/commit/74b1ad43020a11ca27e60e3a24de0db5d1ab37b4))
2026-08-27 18:22:01 +00:00
asepharyana 74b1ad4302 perf(agent): eksekusi tool read-only paralel seperti Claude Code
Sebelumnya loop utama mengeksekusi semua tool call satu-per-satu
(sequential for loop). Seperti Claude Code, tool read-only yang
independen dalam satu pesan assistant (read/grep/glob/semantic_search
dsb.) kini dijalankan konkuren dengan bounded parallelism (max 8),
mengurangi latensi per turn secara signifikan untuk beban coding.

- feat(registry): tool_is_parallel_safe() — whitelist tool read-only
  yang aman dijalankan paralel; tool mutating/shell tetap sequential
- feat(executor): is_parallel_safe() delegasi ke registry; ToolExecutor
  trait Default=false (konservatif)
- fix(application): execute_tool_calls_in_parallel() — join_all +
  semaphore bounded 8, hasil dikumpulkan dalam URUTAN panggilan asli
  (kontrak OpenAI/Anthropic tool-result ordering)
- loop utama: batch paralel hanya jika SEMUA tool parallel-safe; jika
  ada satu tool mutating, jatuh balik ke jalur sequential aman
- test: +2 registry test, +2 application test (konkurensi & urutan,
  fallback batch mutating)
2026-08-28 01:17:26 +07:00
semantic-release-bot 9ad04cf819 chore(release): 1.19.5 [skip ci]
## [1.19.5](https://github.com/asepharyana/zesdex/compare/v1.19.4...v1.19.5) (2026-08-27)

### Bug Fixes

* **api:** model Opus default pakai claude-opus-5 (bukan -4-8) ([b28a5fe](https://github.com/asepharyana/zesdex/commit/b28a5fe384fd45255a6b249c7febd3b4ffc0fd2f))
* **api:** update zesdex packages to version 1.19.4 ([b46935c](https://github.com/asepharyana/zesdex/commit/b46935c606d4f68ea227c7db27e4c2aa9b4e373c))
2026-08-27 17:36:59 +00:00
asepharyana b46935c606 fix(api): update zesdex packages to version 1.19.4 2026-08-28 00:32:27 +07:00
asepharyana b28a5fe384 fix(api): model Opus default pakai claude-opus-5 (bukan -4-8)
Model terbaru di 9router adalah claude-opus-5. Update semua jalur
model default Opus:

- fix(app_config_repo): fallback default_model custom_model.unwrap_or
  -> claude-opus-5; model_roles list claude-opus-5
- fix(app_config): router provider default_model -> claude-opus-5
- fix(settings test): assertion claude-opus-5
- fix(data): ~/.local/share/zesdex/settings.json model -> claude-opus-5
2026-08-28 00:32:06 +07:00
semantic-release-bot b66898ea28 chore(release): 1.19.4 [skip ci]
## [1.19.4](https://github.com/asepharyana/zesdex/compare/v1.19.3...v1.19.4) (2026-08-27)

### Bug Fixes

* **api:** model claude selalu pakai Opus dari settings.json, bukan deepseek ([9aca45c](https://github.com/asepharyana/zesdex/commit/9aca45cb65d6913d14fecc10d70c180934f69d74))
2026-08-27 17:05:10 +00:00
asepharyana 9aca45cb65 fix(api): model claude selalu pakai Opus dari settings.json, bukan deepseek
TUI turn.rs & daemon handler.rs ambil model langsung dari
settings.model (tersimpan 'deepseek-v4-flash-free' di
~/.local/share/zesdex/settings.json) padahal provider sudah 'claude'.

- feat(domain): resolve_effective_model() — saat provider claude, model
  diambil dari app_config provider claude (default_model=claude-opus-4-8
  hasil deteksi ~/.claude/settings.json), menang atas settings.model basi.
  Provider non-claude tetap hormati settings.model user.
- fix(tui): turn.rs pakai resolve_effective_model (bukan settings.model)
- fix(daemon): handler.rs run_turn + compaction pakai resolve_effective_model
- fix(data): ~/.local/share/zesdex/settings.json model deepseek -> claude-opus-4-8
- test: 3 unit test resolve_effective_model
2026-08-28 00:00:32 +07:00
semantic-release-bot 4dccf0cee4 chore(release): 1.19.3 [skip ci]
## [1.19.3](https://github.com/asepharyana/zesdex/compare/v1.19.2...v1.19.3) (2026-08-27)

### Bug Fixes

* **api:** model Opus pakai URL + API custom dari ~/.claude/settings.json ([1f91b44](https://github.com/asepharyana/zesdex/commit/1f91b447080e7106201d77585edb7d38b74e34bb))
2026-08-27 16:49:19 +00:00
asepharyana 1f91b44708 fix(api): model Opus pakai URL + API custom dari ~/.claude/settings.json
Perbaiki provider claude agar selalu refresh dari settings.json dan
menjadi default (claude-opus-4-8) setiap startup:

- fix(app_config_repo): ganti or_insert -> insert untuk provider claude —
  base_url/key dari ~/.claude/settings.json selalu di-refresh, tidak
  tertutup snapshot lama app_config.json.
- fix(app_config_repo): hapus kondisi default_provider == default — saat
  settings.json terdeteksi, default_provider='claude' dan
  default_model='claude-opus-4-8' SELALU di-set (sebelumnya skip kalau
  user pernah ganti provider).
- fix(subagent/provider): resolve_subagent_provider fallback ke
  app_config.default_provider/default_model kalau settings.provider/model
  kosong — subagent ikut pakai Opus.
- test: 4 unit test (parse settings.json, refresh stale provider, custom
  model, env fallback). Verified live: settings.json terbaca (9router URL
  + key).
2026-08-27 23:45:30 +07:00
semantic-release-bot b25929824a chore(release): 1.19.2 [skip ci]
## [1.19.2](https://github.com/asepharyana/zesdex/compare/v1.19.1...v1.19.2) (2026-08-27)

### Performance Improvements

* **agent:** stabilkan async & parallel — satu runtime, bounded concurrency, isolasi error ([6a98d52](https://github.com/asepharyana/zesdex/commit/6a98d52d54a69f78710d852a69dda3ac0a4ead31))
2026-08-27 16:30:31 +00:00
asepharyana 3fd9a2b2db chore: sinkronkan Cargo.lock dengan versi 1.19.1 2026-08-27 23:26:37 +07:00
asepharyana 6a98d52d54 perf(agent): stabilkan async & parallel — satu runtime, bounded concurrency, isolasi error
Seperti Claude Code: satu runtime shared, concurrency dibatasi, error
subagent terisolasi (satu node gagal tidak menggagalkan cycle).

- feat(runtime): global tokio runtime via OnceLock — ganti 9+ titik
  Runtime::new() per tool call (spawn, parallel_delegate, workflow,
  explore, dir_cache, daemon handler). Hemat resource, hilangkan panic
  path Runtime::new().expect() di daemon compaction.
- fix(workflow): execute_cycle ganti try_join_all (fail-fast) →
  buffer_unordered(8) + isolasi error per node; node gagal di-log dan
  diganti [ERROR], hasil node lain tetap dipakai (Claude Code-style).
- fix(parallel_delegate): spawn subagent dibatasi per batch max_parallel
  (tidak unbounded threads).
- perf(subagent): run_agent adaptif max_tokens (800/1600/4096), temp 0.2,
  truncate tool output 12k, error-recovery note utk tool error berulang.
- test: runtime singleton + block_on (2 test).
2026-08-27 23:25:44 +07:00
semantic-release-bot 3847c0e6fd chore(release): 1.19.1 [skip ci]
## [1.19.1](https://github.com/asepharyana/zesdex/compare/v1.19.0...v1.19.1) (2026-08-27)

### Performance Improvements

* **agent:** rombak alur AI agent — adaptif, hemat token, self-healing ([eac0443](https://github.com/asepharyana/zesdex/commit/eac0443c4c3b8bfcbefd4bad9554168fb6525b94))
2026-08-27 16:10:48 +00:00
asepharyana 5023e5dfa1 chore: sinkronkan Cargo.lock dengan versi 1.19.0 2026-08-27 23:06:57 +07:00
asepharyana eac0443c4c perf(agent): rombak alur AI agent — adaptif, hemat token, self-healing
Ganti explore phase MANDATORY (3 subagent tiap turn, boros) dengan
tool explore_codebase yang DIPUTUSKAN agent sendiri (lazy, token-aware):
- hapus ExploreService trait + with_explore + Phase 0 dari turn loop
- ExploreServiceImpl kini jadi tool 'explore_codebase' (1 context-scout
  subagent, read-only, cap output 4k chars)
- system prompt: instruksi TOKEN BUDGET (jawab langsung utk query simple,
  panggil explore_codebase sekali utk task kompleks)

Loop utama kini adaptif & self-healing:
- max_tokens adaptif (800/1600/4096 by request length) — bukan selalu 4096
- temperature 0.2 saat tool-calling, 0.7 utk final answer
- ErrorTracker: deteksi tool error berulang → inject recovery note,
  stop setelah 8 error total (bukan 50 iterasi sia-sia)
- auto-compact history > 60k chars sebelum LLM call
- tool output di-truncate ke 12k chars sebelum masuk konteks

Tambah 8 unit test (truncation, adaptive tokens, error tracker).
2026-08-27 23:06:37 +07:00
asepharyana 14f3eae62a a 2026-08-27 23:06:37 +07:00
semantic-release-bot aec53651ed chore(release): 1.19.0 [skip ci]
# [1.19.0](https://github.com/asepharyana/zesdex/compare/v1.18.4...v1.19.0) (2026-08-27)

### Features

* hapus fitur LSP bawaan (language server protocol) ([93f3c2a](https://github.com/asepharyana/zesdex/commit/93f3c2a3572511b5a84f244980ad71bd1b455e70))
2026-08-27 15:49:38 +00:00
asepharyana 93f3c2a357 feat: hapus fitur LSP bawaan (language server protocol)
Hapus seluruh pipeline LSP (client, manager, provisioner, dan 7 tool
lsp_*) dari codebase:

- apps/infrastructure/src/lsp/ (client.rs, manager.rs, provisioner/*)
- apps/infrastructure/src/tools/lsp/ (connect, disconnect, diagnostics,
  hover, completion, definition, references)
- ToolCtx/ToolCtxBuilder: hapus field lsp_manager
- Daemon state: hapus lsp_manager, lsp_provision_msgs, shutdown_lsp
- Registry: hapus registrasi 7 tool lsp_*
- Settings: hapus lsp_auto_provision + lsp_languages
- Agent definitions: hapus lsp_* dari allowed tools coder/reviewer
- Cargo: hapus dependency lsp-types (workspace + infra)
- Update dokumentasi mod + arch_audit forbidden list

Verifikasi: cargo check/clippy/test semua hijau (54 test), tidak ada
referensi lsp_* tersisa di luar CHANGELOG.
2026-08-27 22:38:21 +07:00
semantic-release-bot 9ea3d361b1 chore(release): 1.18.4 [skip ci]
## [1.18.4](https://github.com/asepharyana/zesdex/compare/v1.18.3...v1.18.4) (2026-08-27)

### Performance Improvements

* **tui:** render streaming token secara inkremental + kurangi redraw sia-sia ([2717216](https://github.com/asepharyana/zesdex/commit/271721694beb62e9fa5ff932312b293aa1d56823))
2026-08-27 15:33:34 +00:00
asepharyana 271721694b perf(tui): render streaming token secara inkremental + kurangi redraw sia-sia
- fix(view): token streaming kini benar-benar tampil — sebelumnya cache
  display_lines tidak pernah di-rebuild saat pesan terakhir berubah
  (msg_count == cached_count), jadi teks AI streaming tidak pernah muncul
  sampai pesan baru/resize
- perf(view): streaming kini hanya re-render pesan TERAKHIR (splice di
  batas cached_last_start) → O(konten baru) per token, bukan O(seluruh
  history); guard cached_last_len mencegah re-render pada frame spinner
  tanpa token baru
- perf(run): skip chrono::Utc::now() + drain toasts saat tidak ada toast
- perf(misc): drain_expired_toasts tidak lagi clone seluruh daftar toast
- perf(view): render_toasts fast-path saat toasts kosong
2026-08-27 22:23:28 +07:00
semantic-release-bot 171597ca05 chore(release): 1.18.3 [skip ci]
## [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))
2026-08-27 15:19:16 +00:00
asepharyana 7b0b53671f style: format seluruh workspace dengan cargo fmt
Menyeragamkan format kode sesuai rustfmt (126 file). Sebelumnya
lefthook pre-commit 'cargo fmt --check' akan gagal pada commit apa pun.
2026-08-27 22:10:28 +07:00
asepharyana 884b19ccb5 fix(api): cegah race condition pada register users.json (TOCTOU)
- Tambah users_lock (Mutex) di ApiState untuk serialisasi read-modify-write
  users.json pada endpoint register; lock hanya dipegang selama operasi
  file sinkron (tidak pernah lintas .await, menjaga future tetap Send)
- Hash password dihitung sebelum lock sehingga request concurrent tidak
  saling blokir selama hashing Argon2
2026-08-27 22:09:22 +07:00
asepharyana 6db00b2266 fix(api): perbaiki keamanan auth & WebSocket, tambah rate limiting
Security fixes hasil audit:
- fix(auth): refresh token kini memakai claim typ=refresh; access token
  tidak bisa dipakai sebagai refresh token (sebelumnya bisa — eskalasi
  masa berlaku 1 jam -> 7 hari)
- fix(api): layer JWT hanya melindungi route /sessions dan /chat;
  /auth/login, /auth/register, /auth/refresh, /health kini publik
  (sebelumnya semua route 401-lock, API tidak bisa dipakai sama sekali)
- fix(ws): endpoint /ws kini memverifikasi token ZESDEX_WS_TOKEN via
  query param jika env diset (mencegah pemakaian LLM proxy terbuka)
- feat(api): rate limiting login/register/refresh (20 request / 10 menit
  per client IP) memakai RateLimiter yang tadinya dead code
- test(jwt): tambah unit test token type access vs refresh + expired
2026-08-27 22:04:32 +07:00
asepharyana 7f64423615 fix(build): perbaiki referensi paket zesdex-gateway di Dockerfile & default.nix
- Dockerfile & default.nix: ganti zesdex-backend -> zesdex-gateway (binary zesdex)
- flake.nix & default.nix: versi 1.13.0 -> 1.18.2 mengikuti Cargo.toml
- chore(ci): drop cargo build --release dari check job, tambah cargo fmt --check
2026-08-27 21:52:25 +07:00
asepharyana 6d3f4918bf fix(build): perbaiki referensi paket zesdex-gateway dan sinkronisasi versi nix
- Dockerfile & default.nix: ganti zesdex-backend -> zesdex-gateway (binary zesdex)
- flake.nix & default.nix: versi 1.13.0 -> 1.18.2
- Cargo.lock: sinkronkan versi workspace member (1.18.0 -> 1.18.2)
- perf(search): kurangi alokasi per-query di semantic search (Vec<String> -> Vec<&str>)
- chore(ci): drop cargo build release dari check job, tambah fmt check
- chore: tambah .dockerignore, perbaiki trailing newline .gitignore
2026-08-27 21:50:35 +07:00
asepharyana 448eb5d462 delete: remove architecture, backend, data, dependencies, development, and frontend documentation files
add: create flake.lock for Nix package management
2026-08-27 21:30:42 +07:00
460 changed files with 10978 additions and 38661 deletions
-48
View File
@@ -1,48 +0,0 @@
# Strict Rust compiler configuration for zesdex
# Forces all warnings as errors, enables maximum optimization for release builds.
[target.'cfg(all())']
# Treat all warnings as errors — zero tolerance policy
rustflags = [
# Force all lints to error level (stricter than -Dwarnings)
"-W", "unused",
"-W", "dead_code",
"-W", "unreachable_code",
"-W", "unused_imports",
"-W", "unused_variables",
"-W", "unused_mut",
"-W", "unused_must_use",
"-W", "unused_unsafe",
"-W", "unused_extern_crates",
# Safety-critical
"-W", "trivial_casts",
"-W", "trivial_numeric_casts",
# Deprecation and future compat
"-W", "deprecated",
"-W", "deprecated_in_future",
"-W", "keyword_idents",
"-W", "noop_method_call",
# Type system
"-W", "invalid_type_param_default",
"-W", "unused_labels",
"-W", "while_true",
]
[profile.release]
# Maximum speed optimizations for release builds
opt-level = 3
debug = false
debug-assertions = false
overflow-checks = true
lto = "fat"
codegen-units = 1
panic = "abort"
strip = "symbols"
[profile.dev]
# Keep dev fast but still strict
opt-level = 0
debug = true
-94
View File
@@ -1,94 +0,0 @@
---
name: commit-and-push
description: Enforce Conventional Commits specification for all commit messages and push workflows
---
# Commit and Push Rule
All commits MUST follow the [Conventional Commits v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) specification. No exceptions.
## Commit Message Format
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
## Types
| Type | When to use |
|------|-------------|
| `feat` | New feature — correlates with `MINOR` in semver |
| `fix` | Bug fix — correlates with `PATCH` in semver |
| `chore` | Maintenance, deps, config — no production code change |
| `docs` | Documentation only |
| `style` | Formatting, whitespace — no logic change |
| `refactor` | Code restructure — no feature or fix |
| `perf` | Performance improvement |
| `test` | Adding or correcting tests |
| `build` | Build system or external dependency changes |
| `ci` | CI configuration and scripts |
| `revert` | Reverts a previous commit |
## Breaking Changes
Append `!` after type/scope to indicate a breaking change. This correlates with `MAJOR` in semver.
```
feat(api)!: remove deprecated /v1/users endpoint
BREAKING CHANGE: /v1/users has been removed. Use /v2/users instead.
```
A `BREAKING CHANGE:` footer can also be used in the commit body.
## Rules
1. Type is ALWAYS lowercase.
2. Description is imperative mood ("add", not "added" or "adds").
3. Description is lowercase, no trailing period.
4. Keep subject line under 72 characters.
5. Scope is optional but recommended for `feat` and `fix` — use the affected module name.
6. One logical change per commit. If a commit spans multiple types, split into multiple commits.
7. Body wraps at 72 characters. Use it to explain **why**, not **what**.
8. Footer uses `git trailer` format (e.g., `BREAKING CHANGE:`, `Reviewed-by:`, `Refs:`).
## Lefthook Hooks
Every commit and push MUST go through Lefthook's `pre-commit` and `pre-push` hooks. Hooks are the gatekeeper — if they fail, the commit/push does not happen.
1. `pre-commit` runs lint-staged on staged files. Commit is blocked until lint-staged passes.
2. `pre-push` runs lint-staged diff check and version bump. Push is blocked until both pass.
3. If a hook fails, **fix the root cause**. Do not work around it.
## Push
1. Every push MUST pass pre-commit and pre-push hooks (see `push-flow-convention` skill).
2. **NEVER use `--no-verify`** to bypass hooks. No exceptions. No "just this once." If hooks fail, fix the issue and retry.
3. **NEVER use `git commit --no-verify`**. If pre-commit fails, fix linting/formatting and restage.
4. **NEVER use `git push --no-verify`**. If pre-push fails, fix the failing check and push again.
5. Commit message quality is enforced — reject vague messages like "fix stuff", "update", "wip", "misc".
6. If Lefthook is not installed, run `pnpm exec lefthook install` before committing. Do not commit without hooks registered.
7. **NEVER add `Co-Authored-By` trailers for AI tools** (e.g., `Co-Authored-By: Claude Code <noreply@anthropic.com>`). Commits are authored by humans only. No AI attribution in commit messages.
8. **NEVER stage all files in one commit** (`git add .` or `git add -A` then commit). Group related changes into separate, focused commits. Each commit = one logical change. If a feature touches auth + billing, split into separate commits per module.
9. Stage files deliberately by name (`git add src/auth/login.ts src/auth/types.ts`). Review what's staged before committing (`git status`, `git diff --cached`).
## Examples
```
feat(auth): add google oauth sign-in
fix(cart): correct total calculation when discount is zero
chore: update eslint config
docs: add setup guide to README
refactor(billing): extract invoice calculation to service layer
test(users): add unit tests for avatar upload
perf(api): cache user profile queries
feat(api)!: change response format for /orders endpoint
```
## Reference
Full specification: [conventionalcommits.org/en/v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/)
-58
View File
@@ -1,58 +0,0 @@
---
name: docs-folder
description: Route non-hexagonal files to docs/ folder to keep domain architecture clean
---
# Docs Folder Rule
Any file that does not fit the hexagonal architecture design pattern MUST live in the `docs/` folder. The source tree stays clean — only hexagonal-compliant code belongs in `src/`.
## Hexagonal Architecture Recap
```
src/
├── domain/ # Pure business logic, entities, value objects, ports (interfaces)
├── application/ # Use cases, orchestration, input/output ports
├── infrastructure/ # Adapters — DB, HTTP clients, messaging, external APIs
└── interfaces/ # Controllers, routes, CLI, resolvers (driving adapters)
```
Only code that fits one of these layers belongs in the source tree.
## What goes in `docs/`
| Item | Why it's not hexagonal |
|------|----------------------|
| Architecture decision records (ADRs) | Documentation, not code |
| API documentation / OpenAPI specs | Reference material |
| Database diagrams / ERDs | Design artifacts |
| Flowcharts / sequence diagrams | Visual documentation |
| Meeting notes / technical decisions | Project context |
| Onboarding guides | People documentation |
| RFC / proposal documents | Decision records |
| Scratch files / experiments | Not production code |
| Third-party integration guides | Reference material |
| Deployment runbooks | Ops documentation |
| Configuration examples / templates | Not domain logic |
| Migration guides / upgrade notes | Process documentation |
## Structure
```
docs/
├── adr/ # Architecture Decision Records
├── api/ # API specs, OpenAPI/Swagger files
├── diagrams/ # ERDs, flowcharts, sequence diagrams
├── guides/ # Onboarding, deployment, migration guides
├── rfcs/ # Proposals and RFCs
└── notes/ # Meeting notes, scratch, experiments
```
## Non-negotiables
1. NEVER put documentation files in `src/` — they pollute the domain.
2. NEVER put scratch code, experiments, or spikes in `src/` — use `docs/notes/` or a separate branch.
3. NEVER put config examples or templates in `src/` — use `docs/` or project root.
4. If a file doesn't implement a port, adapter, use case, or entity — it doesn't belong in `src/`.
5. Keep `docs/` organized by category, not by date or author.
6. README at project root is fine — detailed docs go in `docs/`.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,144 +0,0 @@
# 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.
@@ -1,135 +0,0 @@
# 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.
File diff suppressed because one or more lines are too long
@@ -1,161 +0,0 @@
# Programming Paradigms
When to load this reference: when choosing between procedural and OO style, writing code in a functional language, refactoring switch statements, handling persistence, or when the user asks about OO vs FP, design patterns, or Clean Code's chapter on objects and data structures.
Uncle Bob's reductionist framing of the three paradigms is a powerful lens for reasoning about code shape. Each paradigm imposes **discipline** by **taking something away** from the programmer.
---
## The Three Paradigms
Each paradigm is defined by what it *forbids*, not by what it enables. This is Dijkstra-style reasoning: fewer primitives mean fewer ways to be wrong.
### Structured Programming
- **Forbids:** `goto` (direct transfer of control).
- **Provides:** Sequence, Selection (if/else), Iteration (while). Dijkstra proved any algorithm can be expressed with just these three.
- **Why:** Dijkstra's 1968 letter "Go To Statement Considered Harmful." Unrestricted `goto` makes programs impossible to reason about. Restricted control flow is provably correct for sequence, selection, iteration; not provably correct with arbitrary `goto`.
- **Status today:** Won so completely that most developers don't even realize they're using it. Modern languages don't have `goto` (or discourage it).
### Object-Oriented Programming
- **Forbids:** Raw function pointers / indirect transfer of control through unmanaged pointers.
- **Provides:** Polymorphism. The language manages the function pointers for you.
- **Why:** Raw function pointers (as in C) are correct but fragile — every caller must follow conventions every time. Polymorphism provides the same runtime capability through a disciplined mechanism: objects carry their own dispatch table, set up once when the object is created.
- **The reductionist core:** OO = polymorphism. Encapsulation, methods-bound-to-data, and simple inheritance exist in C and Pascal too. **What OO uniquely gives you is convenient polymorphism.** "OO without polymorphism is not OO."
### Functional Programming
- **Forbids:** Assignment / mutation of state.
- **Provides:** Referential transparency. Same inputs → same outputs, always, everywhere.
- **Why:** Shared mutable state is the source of most concurrency bugs and most "action at a distance" reasoning failures. Forbidding it means state changes are explicit and localized.
- **The reductionist core:** FP = referential transparency. Higher-order functions exist in OO languages too (Smalltalk, etc.). What FP uniquely gives you is the guarantee that a function call cannot change anything you didn't pass to it.
### Why "Three Paradigms" Matters
These are **orthogonal**, not competing. Each removes a different freedom:
| Paradigm | Discipline on | Mechanism |
|---|---|---|
| Structured | Direct transfer of control | No `goto` |
| OO | Indirect transfer of control | Polymorphism |
| FP | Assignment | Referential transparency |
A language can (and modern ones often do) impose all three disciplines at once. You can write OO code functionally, and you can apply SOLID inside a functional program.
---
## OO and FP Are Orthogonal, Not Exclusive
From Uncle Bob's 2014 and 2018 "FP vs OO" posts:
> "The principles of software design still apply, regardless of your programming style. The fact that you've decided to use a language that doesn't have an assignment operator does not mean that you can ignore the Single Responsibility Principle; or that the Open Closed Principle is somehow automatic."
And from his 2023 *Functional Classes* post: "Should you subdivide a functional program into classes the way you would an object oriented program? Yes. You should. Because the rules don't change just because you've chosen to use immutable data structures."
**A class, reductively:** "A group of cohesive and narrowly defined functions that operate on an encapsulated data structure. The functions may, or may not, be polymorphically deployed." This definition works in Clojure, Haskell, Rust, Java, TypeScript, Python.
**The design principles transcend paradigm:**
- SRP applies in Clojure (group functions by actor).
- OCP applies in Haskell (use abstraction, add type class instances).
- DIP applies anywhere there are modules.
- A "class" in the sense above is a cohesive namespace of related functions plus the data they operate on.
---
## Data/Object Anti-Symmetry
From Chapter 6 of *Clean Code* and elaborated in the 2019 blog post "Classes vs. Data Structures."
**Two definitions that complement each other:**
- **Object:** A set of functions that operate on **implied** data. Data exists but is hidden. Callers see only functions.
- **Data structure:** A set of data elements operated on by **implied** functions. Data is exposed. Functions exist but are not specified by the structure.
They are **diametric opposites**. You cannot fully be both.
### Consequences
- **DTOs are data structures, not objects.**
- **Database tables are data structures, not objects.**
- **"ORM" is a misnomer.** There is no mapping between database tables and objects. ORMs map tables to data structures. (This is not pedantic; it explains why ORMs have the smells they do.)
- **Polymorphism is the marker of objects.** When `shape.area()` dispatches dynamically to the Circle or Square implementation, you are doing OO. When `area(shape)` is a free function with `match shape { Circle => …, Square => … }`, you are doing procedural work.
### The Four Symmetry Rules
These tell you when to choose each style.
| | Add new FUNCTION | Add new TYPE |
|---|---|---|
| **Classes (OO)** | **Hard** — change every class | **Easy** — add one class |
| **Data structures (procedural)** | **Easy** — add one function | **Hard** — change every function |
**Choose by expected axis of change:**
- If you expect more new functions than new types → procedural style with data structures + functions (e.g., visitor pattern, pattern matching over enums, Clojure-style).
- If you expect more new types than new functions → OO style with classes and polymorphism.
- The **Visitor pattern** is procedural-style behavior over OO data — it bridges the two.
**In Rust specifically:** enums with `match` are procedural by this taxonomy (add a variant → every match must handle it); traits with implementations are OO (add an impl → no existing code changes). Neither is wrong; choose by axis of change. If new variants are rare and new operations are common, the enum wins. If new types are common, the trait wins.
---
## Polymorphism and if-else-switch
From "if-else-switch" (2021). A very common refactor:
**The pattern.** When you see an if/else chain or switch that branches by type or by "kind," replace it with:
1. A base class or interface with one method per case.
2. Concrete implementations, one per branch.
3. A **factory** that creates the right implementation based on the discriminator (this is where the if/else/switch ends up, condensed into one place).
4. The business logic calls the interface, never the discriminator.
**Runtime characteristics are identical.** If/else does a procedural lookup, switch uses a compiler-built jump table, polymorphic dispatch uses a vtable — similar performance.
**What you gain:**
- The high-level business code no longer transitively depends on every low-level case.
- Each case is its own named method, not an indented block within a branch.
- New cases = new classes (OCP).
- Independent deployment becomes possible: the high-level module and each implementation can live in separate components.
**When not to apply:** if the switch is small, stable, and not type-based (e.g., processing a small enum of flags in one place), leaving it as a switch is fine. The rule is "factor out switches on *type*," not "destroy every conditional."
---
## The Tell-Don't-Ask Style
Alan Kay's original OO conception: objects as cells in a biological system.
> "Neurons are tellers, not askers. Hormones are tellers, not askers. In biological systems, communication was half-duplex."
Instead of:
```
if account.getBalance() < amount:
throw InsufficientFunds
account.setBalance(account.getBalance() - amount)
```
Say:
```
account.withdraw(amount) // account decides if it can, and how
```
The caller stops interrogating state and deciding. The object owns the decision. This is what Law of Demeter is a weak shadow of — the deeper principle is that state should not leak out of objects.
---
## Loops and State Machines
From the 2020 "Loopy" post. Any program with nested loops can be refactored step-by-step into a Turing-style finite state machine, with tests passing at every step. This is a useful mental exercise: a nested loop is a state machine that a programmer wrote too compactly.
Practical takeaway: when a loop body is getting complex, consider extracting an explicit state (enum of states) and transitioning between them. Reads better than four nested `if`s; generalizes better; easier to test.
---
## Applying This in Practice
- **Default to OO + polymorphism** for business logic where types vary (entities, strategies, handlers). Polymorphism is the mechanism behind DIP, OCP, and Clean Architecture boundaries.
- **Default to data structures + free functions** for values, messages, and records that flow through the system. DTOs, events, API payloads, DB rows.
- **Keep the two species apart.** A "hybrid" that has both public fields and rich behavior usually gets the worst of both worlds.
- **FP is not an exception to SOLID.** Cohesion, SRP, DIP all still apply; you express them with namespaces, protocols, or type classes instead of classes.
File diff suppressed because one or more lines are too long
-177
View File
@@ -1,177 +0,0 @@
# Test Driven Development
When to load this reference: when writing new tests, reviewing tests, debugging brittle tests, dealing with legacy code that resists testing, or deciding on a testing strategy for a module.
Tests are the safety net that makes fearless refactoring possible. Without that net, every change is a gamble; with it, every change can be confident. Tests are also the most precise, executable documentation a system will ever have.
**Michael Feathers's definition of legacy code:** *Legacy code is code without tests.* Uncle Bob adopted this definition and it underpins the TDD practice.
---
## The Three Laws of TDD
1. **You are not allowed to write any production code unless it is to make a failing unit test pass.**
2. **You are not allowed to write any more of a unit test than is sufficient to fail — and compilation failures are failures.**
3. **You are not allowed to write any more production code than is sufficient to pass the one failing unit test.**
The loop is measured in seconds, not minutes. Write a line or two of test, see it fail, write a line or two of production, see it pass, repeat. This is the **nano-cycle**.
**Why these rules:**
- **Debugging time plummets** — you were never more than 60 seconds away from working code.
- **Tests are automatic documentation** that cannot fall out of sync with the system.
- **Design improves** because code written to be testable is naturally decoupled.
- **Refactoring becomes fearless** because the net catches regressions instantly.
This is double-entry bookkeeping for software. Every behavior is stated twice — once in the test, once in the code — and they must agree.
---
## F.I.R.S.T. — Clean Tests
Clean tests are:
- **Fast.** Slow tests will stop being run. If a suite takes 10 minutes, people will commit without running it. 15-minute CI feedback is too slow for the TDD loop.
- **Independent.** No test depends on another. Any test can run alone, in any order.
- **Repeatable.** Same result in every environment — laptop, CI, staging. If a test depends on the network, wall clock, or shared database, it is flaky and must be fixed.
- **Self-validating.** Pass or fail. No manual inspection.
- **Timely.** Written *just before* the production code they cover — not "when we have time."
Test code is first-class. Hold it to the same clarity bar as production code. When tests rot, production code rots.
---
## Canonical Test Definitions (First-Class Tests, 2017)
The industry has been sloppy about what "unit," "integration," "acceptance," etc. mean. Uncle Bob's proposed taxonomy:
- **Unit Test.** Written by a programmer, for a programmer. Ensures production code does what the programmer expected. Sometimes called **programmer test** or **micro-test**.
- **Acceptance Test.** Written by the business (or a BA/QA representing the business). Ensures production code does what the business expects. Sometimes called **customer test**.
- **Integration Test.** Written by architects or technical leads. Ensures a sub-assembly of system components operates correctly. **These are plumbing tests, not business-rule tests** — rules are already verified by unit and acceptance tests.
- **System Test.** An integration test for the whole integrated system.
- **Micro-test** (Mike Hill / @GeePawHill). A unit test at very small scope — tests a single function or small group.
- **Functional Test.** A unit test at larger scope, with mocks for slow components.
> "Integration tests do not test business rules. Those rules have already been tested, once by programmer (unit) tests, and again by customer (acceptance) tests. Integration tests test the plumbing and choreography of the components." — Uncle Bob (Twitter, 2019)
**Implication for Claude when writing tests:** Know which kind of test you are writing and don't couple it to the wrong kind. If you're asked to "add tests" for a pure function, write unit/micro tests. If you're asked to "test the API works end-to-end," that's integration/system. Don't test business rules in an integration test — the rules should already have unit tests.
---
## Test Structure
Use one of these structures; be consistent.
- **Arrange / Act / Assert** — set up context, perform action, check result.
- **Given / When / Then** — same thing in BDD vocabulary.
- **Build / Operate / Check** — same thing, different vocabulary.
One *concept* per test. Often one assertion, but "one concept" is the real rule — several assertions verifying the same behavior are fine.
### Test Naming
Name the test for what it verifies about behavior, not for the method. `returns_empty_list_when_given_empty_input` beats `test_filter_1`. If the name runs long, the test is probably doing more than one thing.
---
## Test Doubles — The Hierarchy
Adapted from Gerard Meszaros's *xUnit Patterns*, with Uncle Bob's gloss. Each is a degree of sophistication above the last.
- **Dummy.** Passed around but never used. Fills a parameter slot.
- **Stub.** Returns canned answers. No logic.
- **Spy.** A stub that records the calls it received.
- **Mock.** A spy with expectations built in: set up *before* the act, verified *after*. Fails if expected interactions didn't happen.
- **Fake.** A working implementation with production-unfit shortcuts — e.g., in-memory repo that stands in for a real database.
Pick the lowest-sophistication double that does the job. A mock where a stub would suffice adds coupling and fragility.
**Uncle Bob hand-rolls most of his Java mocks** ("Manual Mocking," 2009) rather than using mockito, to keep explicit control over ceremony. This is a taste preference, not a rule, but his reasoning (less magic, clearer test code) is worth knowing.
---
## Chicago vs. London (State-ism vs. Mockism)
Two schools of TDD.
- **Chicago / Classical / State-ist.** Test behavior through state. Exercise the object, assert on its final state (or collaborators' state). Minimal mocking. Less coupled to implementation detail.
- **London / Mockist.** Test behavior through interactions. Mock collaborators; assert on calls. More explicit about collaboration but more coupled to it.
**Practical guidance:** Use Chicago for value objects, algorithms, internal logic. Use London at **boundaries** — where the code coordinates external collaborators. Never mock what you own when you could exercise it directly; mock (or fake) what you do not own when the real thing would make the test slow or flaky.
---
## Fragile Tests
Tests that break without a real regression are worse than no tests — they train developers to ignore the suite. Known causes:
- **Interface sensitivity.** Tests break because a signature changed, not behavior. Often a sign of excessive mocking.
- **Behavior sensitivity.** Tests break because an unrelated behavior changed. A sign of poor isolation.
- **Data sensitivity.** Tests break because shared fixtures changed. Fix by making tests own their data.
- **Context sensitivity.** Tests pass locally, fail in CI. Remove environmental coupling: clock, network, filesystem, time zone.
- **Over-specification.** Tests assert on more than the behavior under test — internal call order, private fields, log output. Assert on what the *user of the code* would observe.
A fragile test is a design signal — usually a missing abstraction, a leaky boundary, or an over-eager mock.
"Skilled TDDers understand that neither micro-tests, nor functional tests, nor acceptance tests should be coupled to the implementation of the system." — *First-Class Tests* (2017)
---
## As Tests Get More Specific, Code Gets More Generic
Uncle Bob's formulation (2009): tests are specifications. As you add tests, the specifications grow more specific. To satisfy them all, the production code must grow more *generic*. This is the inverse relationship that drives TDD-induced good design — the code gets pushed toward abstractions that cover many cases rather than one.
---
## The Transformation Priority Premise (TPP)
When making a failing test pass, there is a natural ordering of changes, simpler before more complex. Prefer earlier transformations when more than one would work:
1. `{} → nil` — no code → returning nil
2. `nil → constant` — return a constant
3. `constant → variable` — replace constant with a variable
4. `statement → statements` — add another statement
5. `unconditional → if` — introduce a branch
6. `scalar → array` — move from a single value to a collection
7. `array → container` — move to a richer collection type
8. `statement → recursion` — replace a statement with recursion
9. `if → while` — replace a branch with iteration
10. `expression → function` — extract a function
11. `variable → assignment` — introduce mutation
Using lower-priority transformations earlier creates needless complexity; using higher-priority ones later often indicates a design that could be simpler. TPP is a tiebreaker, not a law — but it usually guides tests toward algorithms that generalize cleanly.
---
## The Cycles of TDD
TDD operates at multiple time scales simultaneously. Working at only one scale produces bad software.
- **Seconds (Red-Green-Refactor).** The nano-cycle.
- **Minutes (Specific-to-Generic).** Tests grow more specific; code grows more generic.
- **Tens of minutes (Boundary).** Periodically step back and ask whether the module is still well-factored. Extract. Rename. Regroup.
- **Hours (Architecture).** Once a day or so, step back further: are the component boundaries still correct? Does the Dependency Rule still hold?
- **Days (Acceptance).** Acceptance tests (at the feature/use-case level) close the loop with the business.
Skipping the larger cycles is the most common failure mode. Red-Green-Refactor religiously, but never step back to reconsider architecture, and you end up with a suite of fine-grained tests wrapped around a tangled ball of mud.
---
## Testing Across Architectural Boundaries
- **The test boundary** is a first-class part of architecture. Tests live outside the system they test.
- **Do not couple tests to UI frameworks or databases.** If a test needs a browser to exercise a use case, the boundary between use case and UI is broken.
- **Legacy code strategy** (Feathers). Find a seam — a place where behavior can be varied without modifying code. Write a characterization test at that seam to pin down current behavior. Refactor behind the pin. Repeat.
Uncle Bob's position on test placement: "Don't test through UIs. Don't test through web servers. Test as close to the code as you can." — *Testing Like the TSA* (2017)
---
## Common Pitfalls
- **Writing tests after the fact.** Produces tests that confirm whatever the code happens to do, including the bugs. Much lower value than TDD.
- **Slow test suites.** If any unit test takes more than a fraction of a second, isolate it. Keep the unit suite fast and run integration tests separately.
- **Mocking what you own.** Prefer real objects for your own code.
- **Testing implementation details.** Refactors then break tests without any real regression, and people conclude "TDD gets in the way of refactoring." It doesn't — the tests were just wrong.
- **Skipping refactor.** Red-Green-… is not TDD. The third step is where design emerges.
- **Over-coverage religion.** Uncle Bob's ratio for some project types: 20% test-first, 80% test-after is acceptable for controllers/models/views (per *Testing Like the TSA*, 2017). The three laws are guidance for the hottest logic in the system, not dogma for every trivial accessor.
-47
View File
@@ -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
```
@@ -1,449 +0,0 @@
---
name: kana-rust-backend-best-practice
description: Reference guide for building a Rust clean-architecture backend with Axum, SeaORM, Argon2, JWT, and sea-orm-migration. Use when scaffolding a new Rust service, adding a feature (domain + use-case + repository + handler), or reviewing Rust code against the axum-clean-architecture reference layout.
---
# Axum Clean Architecture Skill
Reference stack (see `../axum-clean-architecture`):
| Layer | Tech |
|---|---|
| HTTP framework | Axum 0.8 |
| ORM | SeaORM 1.1 (PostgreSQL via sqlx + rustls) |
| Migrations | sea-orm-migration |
| Auth | Argon2 (password hashing) + jsonwebtoken (JWT) |
| Validation | zod-rs (schema-driven, mirrors Zod) |
| Pagination | paginator-rs + paginator-sea-orm + paginator-axum |
| Observability | tracing + tracing-subscriber |
| Middleware | tower-http (CORS, TraceLayer) |
| Runtime | Tokio (full features) |
| Error handling | anyhow (app-level), typed domain errors |
---
## 0. Workspace layout
```
axum-clean-architecture/
├── Cargo.toml # workspace, resolver = "3"
├── apps/
│ ├── iam/ # core domain library (lib crate)
│ │ └── src/
│ │ ├── domain/ # entities, repository traits, domain errors
│ │ ├── application/ # use cases + port traits
│ │ ├── infrastructure/ # SeaORM repos + auth services
│ │ └── presentation/ # Axum handlers, DTOs, middleware, state
│ ├── gateway/ # binary — assembles router, runs server
│ └── bootstrap/ # binary — seeds permissions/roles/admin
├── .config/ # AppServer, database/env helpers
└── .migrations/ # sea-orm-migration crate
```
The `iam` app is a **library crate**. `gateway` and `bootstrap` depend on it.
---
## 1. Dependency rules (strictly enforced)
```
presentation → application → domain
infrastructure → domain (implements domain traits)
presentation → infrastructure (only to wire AppState)
```
- Domain has **zero** external crate dependencies beyond `uuid`, `chrono`.
- Use cases depend only on port traits — never on concrete infrastructure types.
- Presentation instantiates use cases from `AppState` on every request; use cases are not stored.
---
## 2. Domain layer
### Entity pattern
Plain Rust structs — no derives beyond what domain logic needs. No ORM annotations.
```rust
// domain/user/entity.rs
pub struct User {
pub id: Uuid,
pub email: String,
pub password_hash: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub struct NewUser {
pub id: Uuid,
pub email: String,
pub password_hash: String,
}
#[derive(Default)]
pub struct UserPatch {
pub email: Option<String>,
pub password_hash: Option<String>,
}
```
### Repository trait pattern
Use `impl Future` in trait methods (Rust 2024 edition, no `async_trait` needed).
Always `Send + Sync` on the trait.
```rust
// domain/user/repository.rs
pub trait UserRepository: Send + Sync {
fn find_by_id(&self, id: Uuid)
-> impl Future<Output = Result<Option<User>, RepositoryError>> + Send;
fn find_by_email(&self, email: &str)
-> impl Future<Output = Result<Option<User>, RepositoryError>> + Send;
fn create(&self, user: NewUser)
-> impl Future<Output = Result<User, RepositoryError>> + Send;
fn update(&self, id: Uuid, patch: UserPatch)
-> impl Future<Output = Result<User, RepositoryError>> + Send;
fn delete(&self, id: Uuid)
-> impl Future<Output = Result<(), RepositoryError>> + Send;
fn list(&self, params: &PaginationParams)
-> impl Future<Output = Result<PaginatorResponse<User>, RepositoryError>> + Send;
}
```
### Shared RepositoryError (lives in domain)
```rust
pub enum RepositoryError {
NotFound,
Conflict(String),
Database(String),
}
```
### Domain errors
Per-aggregate. `AuthError` lives in `domain/auth/errors.rs`:
```rust
pub enum AuthError {
InvalidCredentials,
EmailAlreadyExists,
UserNotFound,
PasswordHashFailed(String),
PasswordVerificationFailed(String),
TokenGenerationFailed(String),
InvalidToken(String),
RepositoryError(String),
}
```
---
## 3. Application layer
### Port traits (interfaces for external services)
```rust
// application/auth/ports/password.rs
pub trait PasswordService: Send + Sync {
fn hash(&self, password: &str)
-> impl Future<Output = Result<String, PasswordError>> + Send;
fn verify(&self, password: &str, hash: &str)
-> impl Future<Output = Result<bool, PasswordError>> + Send;
}
```
```rust
// application/auth/ports/token.rs
pub trait TokenService: Send + Sync {
fn generate_auth_tokens(&self, sub: &str)
-> impl Future<Output = Result<(String, String), TokenError>> + Send;
fn verify_access_token(&self, token: &str)
-> Result<String, TokenError>;
}
```
### Use case pattern
Generic over port traits and repository traits. Constructed in the handler, not stored.
```rust
// application/user/use_cases/create.rs
pub struct CreateUserCommand { pub email: String, pub password: String }
pub struct CreateUserUseCase<P, R> {
password_service: P,
user_repository: R,
}
impl<P: PasswordService, R: UserRepository> CreateUserUseCase<P, R> {
pub fn new(password_service: P, user_repository: R) -> Self { ... }
pub async fn execute(&self, cmd: CreateUserCommand) -> Result<User, AuthError> {
// 1. guard: check uniqueness
// 2. hash password via port
// 3. create domain entity with Uuid::new_v4()
// 4. persist via repository
// 5. log + return
info!(user_id = %user.id, "user created");
Ok(user)
}
}
```
### Use case naming convention
| File | Struct | Command/Query |
|---|---|---|
| `create.rs` | `CreateXxxUseCase` | `CreateXxxCommand` |
| `update.rs` | `UpdateXxxUseCase` | `UpdateXxxCommand` |
| `delete.rs` | `DeleteXxxUseCase` | `DeleteXxxCommand` |
| `detail.rs` | `XxxDetailUseCase` | `XxxDetailQuery` |
| `list.rs` | `ListXxxsUseCase` | takes `&PaginationParams` |
---
## 4. Infrastructure layer
### SeaORM repository implementation
```rust
// infrastructure/repository/user.rs
#[derive(Clone)]
pub struct SeaOrmUserRepository { db: DatabaseConnection }
// Convert ORM Model → domain entity here (not in domain)
impl From<Model> for User { ... }
// Map DbErr → RepositoryError
fn map_db_err(e: DbErr) -> RepositoryError {
match e {
DbErr::RecordNotFound(_) => RepositoryError::NotFound,
other => { error!(error = %other, "database operation failed"); RepositoryError::Database(other.to_string()) }
}
}
impl UserRepository for SeaOrmUserRepository {
async fn create(&self, user: NewUser) -> Result<User, RepositoryError> {
let model = ActiveModel {
id: Set(user.id),
email: Set(user.email),
password_hash: Set(user.password_hash),
created_at: Set(now),
updated_at: Set(now),
};
let inserted = model.insert(&self.db).await.map_err(|e| match e {
DbErr::Exec(ref msg) | DbErr::Query(ref msg)
if msg.to_string().contains("unique") =>
RepositoryError::Conflict("email already exists".into()),
other => RepositoryError::Database(other.to_string()),
})?;
Ok(User::from(inserted))
}
}
```
Pagination uses `paginator-sea-orm`:
```rust
let response = UserEntity::find()
.paginate_with(&self.db, params)
.await
.map_err(|e| RepositoryError::Database(e.to_string()))?;
let mapped: Vec<User> = response.data.into_iter().map(User::from).collect();
Ok(PaginatorResponse { data: mapped, meta: response.meta })
```
### Auth services
- `Argon2PasswordService`: uses `spawn_blocking` for CPU-bound hashing, `SaltString::generate(OsRng)`.
- `JwtTokenService`: stores `secret: Vec<u8>`, generates separate access/refresh tokens with a `type` claim. `verify_access_token` checks `claims.token_type == "access"`.
### SeaORM entities (ORM models)
Live in `infrastructure/repository/entities/`. One file per table. Junction tables (`user_role`, `role_permission`) have composite primary keys. Timestamps use `DateTimeWithTimeZone`.
---
## 5. Presentation layer
### AppState
Concrete types only — no trait objects. Cheap to clone because `DatabaseConnection` is internally Arc-backed.
```rust
#[derive(Clone)]
pub struct AppState {
pub password_service: Argon2PasswordService,
pub token_service: JwtTokenService,
pub user_repository: SeaOrmUserRepository,
pub role_repository: SeaOrmRoleRepository,
pub permission_repository: SeaOrmPermissionRepository,
}
```
Injected via `Extension(state)` on every handler. Use cases are constructed inside handlers.
### AppError
```rust
pub enum AppError { BadRequest(String), Unauthorized, Forbidden, NotFound, Conflict(String), Internal(String) }
impl IntoResponse for AppError { /* maps to HTTP status + JSON { "error": "..." } */ }
impl From<AuthError> for AppError { ... }
impl From<RepositoryError> for AppError { ... }
impl From<TokenError> for AppError { ... }
```
Internal errors are logged with `tracing::error!` before returning a generic 500 message.
### Handler pattern
```rust
#[instrument(skip_all, fields(actor = %actor.id, email = %req.email))]
pub async fn create(
Extension(state): Extension<AppState>,
Extension(actor): Extension<AuthenticatedUser>,
Json(req): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<UserResponse>), AppError> {
let use_case = CreateUserUseCase::new(
state.password_service.clone(),
state.user_repository.clone(),
);
let user = use_case.execute(req.into()).await?;
Ok((StatusCode::CREATED, Json(user.into())))
}
```
Rules:
- Always `#[instrument(skip_all, fields(...))]` on every handler.
- Use `?` to propagate `AppError` (via `From` impls).
- `201 CREATED` for `POST`, `204 NO_CONTENT` for `DELETE`, `200 OK` for everything else.
- Pagination handlers return `PaginatedJson<Dto>` via `paginator-axum`.
### DTO pattern
```rust
#[derive(Debug, Serialize, Deserialize, ZodSchema)]
pub struct CreateUserRequest {
#[zod(email)]
pub email: String,
#[zod(min_length(8), max_length(128))]
pub password: String,
}
impl From<CreateUserRequest> for CreateUserCommand { ... }
#[derive(Debug, Serialize)]
pub struct UserResponse { pub id: Uuid, pub email: String, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc> }
impl From<User> for UserResponse { ... }
```
- Request structs: `Deserialize + ZodSchema`. Use `#[zod(...)]` for field-level validation.
- Response structs: `Serialize` only. Never expose `password_hash`.
- Conversions: `impl From<Request> for Command` and `impl From<DomainEntity> for Response`.
### Middleware
**Auth middleware** (`presentation/middleware/auth.rs`):
- Extracts `Bearer <token>` from `Authorization` header.
- Calls `state.token_service.verify_access_token(token)`.
- Inserts `AuthenticatedUser { id: Uuid }` into request extensions.
**Permission check** (`presentation/middleware/permission.rs`):
- Called inline from handlers: `ensure_permission(&state, &actor, "users:write").await?`.
- Queries `permission_repository.find_for_user(actor.id)` and checks by name.
### Router assembly
```rust
pub fn build_router(state: AppState) -> Router {
Router::new()
.nest("/auth", auth::router())
.nest("/me", me::router())
.nest("/users", user::router())
.nest("/roles", role::router())
.nest("/permissions", permission::router())
.layer(Extension(state))
}
```
Gateway nests the IAM router at `/api/v1/iam` and adds a health check at `/`.
---
## 6. Migrations (sea-orm-migration)
```rust
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(ColumnDef::new(Users::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Users::Email).string().not_null().unique_key())
...
.to_owned(),
).await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Users::Table).to_owned()).await
}
}
#[derive(DeriveIden)]
pub enum Users { Table, Id, Email, PasswordHash, CreatedAt, UpdatedAt }
```
Naming convention: `m{YYYYMMDD}_{6-digit-seq}_{description}.rs`, e.g. `m20260413_000001_create_users.rs`.
---
## 7. Bootstrap pattern
A separate `bootstrap` binary seeds idempotent system data (permissions, roles, admin user):
```rust
// Check existence before inserting — fully idempotent
if permission_repo.find_by_name("rbac:manage").await?.is_none() {
permission_repo.create(...).await?;
}
```
Standard permissions:
- `rbac:manage`, `users:read`, `users:write`, `roles:read`, `roles:write`, `permissions:read`, `permissions:write`
---
## 8. Adding a new aggregate (checklist)
1. **Domain**: `domain/{name}/entity.rs` (entity + NewXxx + XxxPatch), `domain/{name}/repository.rs` (trait + errors), `domain/{name}/errors.rs` if needed.
2. **Application**: `application/{name}/mod.rs`, `application/{name}/use_cases/{create,update,delete,detail,list}.rs`.
3. **Infrastructure entity**: `infrastructure/repository/entities/{name}.rs` (SeaORM model).
4. **Infrastructure repo**: `infrastructure/repository/{name}.rs` (`From<Model>`, `impl XxxRepository for SeaOrmXxxRepository`).
5. **Add to AppState**: `{name}_repository: SeaOrmXxxRepository`.
6. **Presentation DTO**: `presentation/{name}/dto.rs` (Request + Response with `From` impls).
7. **Presentation handlers**: `presentation/{name}/handlers.rs` (`#[instrument]`, construct use case, return DTO).
8. **Presentation router**: `presentation/{name}/mod.rs` (define routes with `axum_route_macro` or `Router::new().route(...)`).
9. **Nest in `build_router`**.
10. **Migration**: new file in `.migrations/src/` following naming convention.
11. **Bootstrap**: seed any required initial data.
---
## 9. Key conventions
- Edition **2024** — use `impl Future` in traits, not `#[async_trait]`.
- All timestamps are `DateTime<Utc>` in domain; `DateTimeWithTimeZone` in SeaORM models; convert with `.with_timezone(&Utc)`.
- UUIDs generated with `Uuid::new_v4()` in the use case, not the repository.
- Unique-constraint conflicts detected via string match on `DbErr::Exec`/`DbErr::Query` containing `"unique"` — map to `RepositoryError::Conflict`.
- `tracing::instrument` on every handler; log user/actor IDs as structured fields.
- `warn!` for expected failures (wrong password, permission denied), `error!` for unexpected DB errors.
- Response structs never expose internal fields (`password_hash`, internal IDs from junction tables).
@@ -1,107 +0,0 @@
---
name: push-flow-convention
description: Enforce pre-commit/pre-push hooks, lint-staged checks, and semver version bump on every push
---
# Push Flow Convention
Every repository MUST enforce the same pre-commit, pre-push, and versioning flow. No push lands without hooks, lint-staged, and a version bump.
## Required Setup
### 1. Lefthook (pre-commit + pre-push)
> **Always use [Lefthook](https://lefthook.dev/) for git hooks. Never use husky.**
Install once per repo:
```bash
pnpm add -D lefthook lint-staged
pnpm exec lefthook install
```
Create `lefthook.yml` in project root:
```yaml
pre-commit:
commands:
lint-staged:
run: pnpm exec lint-staged
pre-push:
commands:
lint-staged:
run: pnpm exec lint-staged --diff="origin/{push_remote_branch}...HEAD"
bump:
run: pnpm run bump
```
### 2. lint-staged
Declared in `package.json`. Runs ONLY on staged files so commits stay fast.
```json
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
}
```
### 3. Version bump script
`package.json` MUST expose a `bump` script used by `pre-push`:
```json
{
"scripts": {
"bump": "node scripts/bump-version.mjs"
}
}
```
The script inspects the diff between the current branch and its upstream, applies the semver rule below, and writes the new version back to `package.json`. Commit the bump before pushing (amend the previous commit or create a `chore: adjust package.json version (bump)` commit — see `commit-convention`).
## Semver Rules (applied on every push)
The bump is based on the changes in the commits being pushed:
| Change size / kind | Bump |
|--------------------|------|
| `< 5` changed files across pushed commits | **patch** (`x.y.Z`) |
| `>= 5` changed files across pushed commits | **minor** (`x.Y.0`) |
| New feature OR new behaviour (any `feat:` commit) | **major** (`X.0.0`) |
Rules in order of precedence:
1. If ANY commit being pushed is a `feat(...)` → **major** bump.
2. Otherwise, count files changed (`git diff --name-only origin/<branch>...HEAD | wc -l`):
- fewer than 5 → **patch**
- 5 or more → **minor**
The `feat` rule always wins — a new feature is always a major bump regardless of file count.
## Non-negotiables
1. NEVER push without pre-commit and pre-push hooks installed.
2. NEVER bypass hooks with `--no-verify` — if a hook fails, fix the root cause.
3. NEVER push without a version bump. Every push = new version.
4. The bump commit MUST use the `chore: adjust package.json version (bump)` message (see `commit-convention`).
5. lint-staged MUST run on every commit. A green lint-staged is a prerequisite for the commit to be created.
6. If `pnpm` is not the package manager, substitute with `npm` or `yarn` but keep the same flow.
## Quick verification checklist
Before declaring the push flow set up, confirm:
- [ ] `lefthook.yml` exists with `pre-commit` and `pre-push` hooks
- [ ] `pnpm exec lefthook install` has been run (hooks registered in `.git/hooks/`)
- [ ] `package.json` has a `lint-staged` block
- [ ] `package.json` has a `bump` script
- [ ] A dry-run commit triggers lint-staged
- [ ] A dry-run push triggers the version bump
+26
View File
@@ -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/
+15 -11
View File
@@ -7,7 +7,7 @@ on:
branches: [main]
env:
CARGO_TERM_COLOR: always
BUN_INSTALL: "${{ github.workspace }}/.bun"
jobs:
check:
@@ -16,16 +16,20 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Build workspace
run: cargo build --release --workspace
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test workspace
run: cargo test --workspace
- name: Type-check
run: bun run check
- name: Clippy workspace
run: cargo clippy --workspace -- -D warnings
- name: Test
run: bun test
- name: Build CLI bundle
run: bun run build
- name: Smoke — CLI help
run: ./dist/zesdex --help
+19 -17
View File
@@ -1,4 +1,4 @@
name: Build & Deploy (Nix)
name: Build & Deploy (Bun)
on:
push:
@@ -25,26 +25,27 @@ jobs:
with:
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
determinate: false
extra-conf: |
sandbox = false
accept-flake-config = true
bun-version: 1.3.14
- name: Cache Nix
uses: DeterminateSystems/magic-nix-cache-action@v14
- name: Install deps
run: bun install --frozen-lockfile
- name: Build zesdex
id: build
- name: Type-check
run: bun run check
- name: Test
run: bun test
- name: Build binary
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"
bun run build
test -x dist/zesdex && echo "binary OK ($(stat -c%s dist/zesdex) bytes)"
- name: Setup SSH key
if: env.VPS_HOST != '' && env.VPS_HOST != 'null'
env:
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
@@ -56,6 +57,7 @@ jobs:
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
- name: Deploy zesdex to VPS
if: env.VPS_HOST != '' && env.VPS_HOST != 'null'
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"
scp dist/zesdex "$VPS_USER@$VPS_HOST:/usr/local/bin/zesdex"
ssh "$VPS_USER@$VPS_HOST" "chmod +x /usr/local/bin/zesdex && systemctl restart zesdex"
@@ -1,20 +0,0 @@
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
+15 -15
View File
@@ -16,14 +16,17 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Build
run: cargo build --release --workspace
- name: Install
run: bun install --frozen-lockfile
- name: Type-check
run: bun run check
- name: Test
run: cargo test --workspace
run: bun test
release:
name: Semantic Release
@@ -34,24 +37,21 @@ jobs:
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Install semantic-release
- name: Install semantic-release (isolated prefix — do NOT touch repo package.json)
run: |
npm install -D \
mkdir -p /tmp/sr-tools
npm install --prefix /tmp/sr-tools \
semantic-release \
@semantic-release/exec \
@semantic-release/git \
@semantic-release/changelog \
@semantic-release/github
echo "/tmp/sr-tools/node_modules/.bin" >> "$GITHUB_PATH"
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
run: semantic-release
+7 -2
View File
@@ -2,8 +2,13 @@ target/
.env
.claude/settings.local.json
node_modules/
package.json
package-lock.json
.superpowers/
docs/lesson/
.kilo/
.kilo/
.hermes/
# Runtime edit-log artifacts
dist/
apps/**/edit-log/
edit-log/
dist/
+24 -10
View File
@@ -4,15 +4,29 @@
"@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": []
}]
[
"@semantic-release/exec",
{
"prepareCmd": "sed -i 's/\"version\": \"[^\"]*\"/\"version\": \"${nextRelease.version}\"/' package.json && bun install --frozen-lockfile && bun run check && bun run build"
}
],
[
"@semantic-release/git",
{
"assets": ["package.json", "bun.lock", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "dist/zesdex",
"label": "zesdex-${nextRelease.version}-linux-amd64"
}
]
}
]
]
}
-20
View File
@@ -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.
+39 -22
View File
@@ -1,38 +1,55 @@
# AGENTS.md
This file provides guidance to Kilo when working with code in the zesdex repository.
This file provides guidance to AI agents (Kilo / Hermes) working in the zesdex repository.
## Overview
Zesdex is an **AI-native autonomous coding agent**, implemented in **TypeScript / Bun**.
It was rebuilt to be a **pure CLI/TUI tool** — there are **no network server interfaces**
(REST API / WebSocket / gRPC / web / daemon were all removed). The only two entry points:
- `zesdex` — interactive **OpenTUI** (primary interface, wired to the real agent).
- `zesdex --headless "<prompt>"` — one-shot non-interactive turn (headless mode).
## Best Practice Conventions
Zesdex follows Kana Engineering Best Practices:
1. **Clean Architecture** — strict `domain` / `application` / `infrastructure` layering
under `src/`, with thin `interfaces/` for the TUI + CLI.
- `domain` has zero I/O and zero framework dependencies.
- `application` depends only on `domain` (ports + turn service).
- `infrastructure` implements domain ports (LLM client, tool executor, file repos).
- DTOs/value objects cross layer boundaries, not entities.
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 dead code.
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 exported `function`, `class`, `interface`, and `type`
needs a `/** */` doc comment explaining what, flow, why, and return value.
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:`.
4. **Commit Convention** — Conventional Commits in Bahasa Indonesia: `feat(scope):`, `fix(scope):`, `chore:`, `docs:`.
5. **Error Handling** — throw `Error` with clear messages; the turn loop wraps LLM
errors and recovers. Log with `console.*` (redirected to `~/.local/share/zesdex/zesdex.log`).
5. **Error Handling** — `anyhow::Result` and `anyhow::bail!` throughout. Log with `tracing` (never stderr).
6. **Testing** — Bun tests (`bun test`) using `describe/test/expect`. Tests are
F.I.R.S.T. Logic-pure modules (TUI state/action/command/controller) are unit-tested.
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** — no `@ts-ignore` / `any` where a real type exists.
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.
8. **Boy Scout Rule** — Leave every module cleaner than you found it.
## Stack
## Available Agents
- Bun 1.3.14 (packageManager pinned in `package.json`).
- TypeScript strict mode, `tsconfig.json` uses `paths` aliases (`@zesdex/*`) →
`src/*`. `bun build ... --compile` bundles into a single `dist/zesdex` binary.
- `@rust-engineer` — Rust clean architecture specialist (subagent).
- `@code-reviewer` — Code review specialist (subagent).
## Commands
## Available Commands
- `/check` — Run cargo check, clippy, and tests.
- `/audit` — Code quality audit against clean-architecture best practices.
- `/doc` — Generate or update doc comments.
- `bun run check` — `tsc --noEmit` typecheck.
- `bun run test` — run all Bun tests.
- `bun run build` — compile `dist/zesdex` binary.
- `bun run tui` — run the interactive TUI.
- `bun run headless "<prompt>"` — one-shot agent turn.
- `bun run bootstrap` — seed default settings/app_config.
+158
View File
@@ -1,3 +1,161 @@
## [1.23.2](https://github.com/asepharyana/zesdex/compare/v1.23.1...v1.23.2) (2026-09-03)
### Bug Fixes
* **tui:** sliding-window stream reconciler + UI polish ([d86d6ae](https://github.com/asepharyana/zesdex/commit/d86d6aeef2dcd4924ced14f21502a84f7e916a26))
## [1.23.1](https://github.com/asepharyana/zesdex/compare/v1.23.0...v1.23.1) (2026-09-03)
### Bug Fixes
* **tui:** repair layout overflow + streaming dup; polish UI/state ([a54f164](https://github.com/asepharyana/zesdex/commit/a54f1644f5943399568194b4f129185a9d841f1d))
# [1.23.0](https://github.com/asepharyana/zesdex/compare/v1.22.0...v1.23.0) (2026-09-03)
### Bug Fixes
* **auth:** perbaiki base64url encode JWT — string di-base64 dulu sebelum replace chars ([1b130e6](https://github.com/asepharyana/zesdex/commit/1b130e69b36603f894bcd45469b9feef28b18562))
* **ci:** Release workflow install semantic-release tanpa merusak package.json ([8c99c6b](https://github.com/asepharyana/zesdex/commit/8c99c6b1ca94e1d2b80dd59bb96eb24b7b3b2dfa))
### Features
* **api:** port Fase 5 REST API — Bun.serve router + JWT middleware + auth/session/conversation/chat handlers + composition root ([49e7932](https://github.com/asepharyana/zesdex/commit/49e79322b4aa1824467e1c871cf68809c1a424ee))
* **auth:** port 3f auth infrastructure — Argon2 passwords + HS256 JWT tokens ([7e8a160](https://github.com/asepharyana/zesdex/commit/7e8a160b0e918d51dc559f55fa19395368a4d84e))
* **auth:** tambah OAuth loopback server untuk capture authorization-code redirect ([fd58b46](https://github.com/asepharyana/zesdex/commit/fd58b4661fd0928dd461c74a5cb7bc7870ca9692))
* **cli:** port Fase 4 CLI — parser arg, mode dispatch, single-process composition root + REPL loop, bootstrap seed ([c582dc1](https://github.com/asepharyana/zesdex/commit/c582dc1461024d5037b8351a0df6b9b11e967782))
* **cli:** wire mode dispatch ke server interfaces — --api/--ws/--grpc/--web start server; add tsconfig paths ([02a5523](https://github.com/asepharyana/zesdex/commit/02a55235cfadb3e5daf3774a04f6fc6abe5da78d))
* **daemon:** port Fase 5 daemon — IPC agent-driver loop (Submit/Close) + streaming tokens; wire CLI --daemon/--attach ([14d3556](https://github.com/asepharyana/zesdex/commit/14d3556179685587e62873f81a57125037ac5e5a))
* **infra:** port tool system ke TypeScript (37 tools + registry + executor) ([82b51f7](https://github.com/asepharyana/zesdex/commit/82b51f7921e33e912f1244152df509522e44cae3))
* **ipc:** port 3g IPC + bgbash — framed Unix-socket server/client + background job registry, wire bash tools ([4b4730c](https://github.com/asepharyana/zesdex/commit/4b4730cff7ee2a5c43f8906d17282d95de84cf7a))
* **rewrite:** bootstrap monorepo Bun + port domain layer ke TypeScript ([07e84e4](https://github.com/asepharyana/zesdex/commit/07e84e43b386388f75f8293879e49dff1db461ab))
* **rewrite:** tambah application layer — port traits & use cases TypeScript ([03538a4](https://github.com/asepharyana/zesdex/commit/03538a455a93ff554abb45a1e54809c9579763fd))
* **rewrite:** tambah infrastructure — LLM client + full file persistence ([a2c7e5d](https://github.com/asepharyana/zesdex/commit/a2c7e5d44c784508b4fe735ec3434df2407f76de))
* **subagent:** port 3c subagent engine — run_agent loop + spawn/delegate/parallel ([a14a227](https://github.com/asepharyana/zesdex/commit/a14a2275ff90059874166aec8c03bd44e6c7efee))
* **tui:** port Fase 4 TUI logic layer — InputState/Action dispatcher/key controller/slash commands + lightweight ANSI renderer + run loop; 20 test ([8098133](https://github.com/asepharyana/zesdex/commit/8098133ba50135c7a5f9a063221ffdb860e064cb))
* **web): port Fase 5 static file server w/ traversal protection; feat(grpc:** port health stub ([1a6268b](https://github.com/asepharyana/zesdex/commit/1a6268bde58c67b9d3cbabce08306d53f0b3791a))
* **workflow:** port 3d workflow + hive-mind engine — parse, execute, cycle, synthesis, docs ([edc0076](https://github.com/asepharyana/zesdex/commit/edc007619b63d0d91fdad79a6d534c112b8a3981))
* **ws:** port Fase 5 WS server — Bun native WebSocket, ZESDEX_WS_TOKEN guard, prompt turn proxy + streaming token/done/error ([d4d423a](https://github.com/asepharyana/zesdex/commit/d4d423a4cae62562da4ca4f0231f6a4f90481473))
# [1.22.0](https://github.com/asepharyana/zesdex/compare/v1.21.2...v1.22.0) (2026-08-28)
### Features
* **agent:** wire auto-review (review_enabled no-op -> nyata) ([a27e815](https://github.com/asepharyana/zesdex/commit/a27e8151b39da4c8759e8e922ef132212327e413))
## [1.21.2](https://github.com/asepharyana/zesdex/compare/v1.21.1...v1.21.2) (2026-08-28)
### Performance Improvements
* **agent:** symbol index tak pegang mutex global saat rebuild I/O ([3b25e38](https://github.com/asepharyana/zesdex/commit/3b25e3898f9ba2fc1c58b991ad95a8c0bbe98601))
## [1.21.1](https://github.com/asepharyana/zesdex/compare/v1.21.0...v1.21.1) (2026-08-28)
### Bug Fixes
* **agent:** semantic_search symbol index workspace-aware ([104af3a](https://github.com/asepharyana/zesdex/commit/104af3abb9e626c5d00ea87d523248d606d523ee))
# [1.21.0](https://github.com/asepharyana/zesdex/compare/v1.20.2...v1.21.0) (2026-08-28)
### Features
* **agent:** hive-mind consensus synthesis pakai LLM nyata ([f75ff74](https://github.com/asepharyana/zesdex/commit/f75ff740ac2fa63340656e1e8215440f5e48063d))
## [1.20.2](https://github.com/asepharyana/zesdex/compare/v1.20.1...v1.20.2) (2026-08-28)
### Bug Fixes
* **agent:** recall search + memory_dir fallback + bersihkan dead llm_client ([f1f58b9](https://github.com/asepharyana/zesdex/commit/f1f58b9996f8eb58871d44fdd41c2d863874f253))
## [1.20.1](https://github.com/asepharyana/zesdex/compare/v1.20.0...v1.20.1) (2026-08-28)
### Bug Fixes
* **agent:** subagent patuhi tool-calling contract + truncation char-safe ([e982cbe](https://github.com/asepharyana/zesdex/commit/e982cbeb041baea9cd500e2a29862a7eca9e6e17))
# [1.20.0](https://github.com/asepharyana/zesdex/compare/v1.19.6...v1.20.0) (2026-08-28)
### Features
* **agent:** subagent tool paralel + auto-load AGENTS.md + verify cek setelah edit ([21e3ccc](https://github.com/asepharyana/zesdex/commit/21e3ccc891ab886044a5f0a770db710769d1287b))
## [1.19.6](https://github.com/asepharyana/zesdex/compare/v1.19.5...v1.19.6) (2026-08-27)
### Performance Improvements
* **agent:** eksekusi tool read-only paralel seperti Claude Code ([74b1ad4](https://github.com/asepharyana/zesdex/commit/74b1ad43020a11ca27e60e3a24de0db5d1ab37b4))
## [1.19.5](https://github.com/asepharyana/zesdex/compare/v1.19.4...v1.19.5) (2026-08-27)
### Bug Fixes
* **api:** model Opus default pakai claude-opus-5 (bukan -4-8) ([b28a5fe](https://github.com/asepharyana/zesdex/commit/b28a5fe384fd45255a6b249c7febd3b4ffc0fd2f))
* **api:** update zesdex packages to version 1.19.4 ([b46935c](https://github.com/asepharyana/zesdex/commit/b46935c606d4f68ea227c7db27e4c2aa9b4e373c))
## [1.19.4](https://github.com/asepharyana/zesdex/compare/v1.19.3...v1.19.4) (2026-08-27)
### Bug Fixes
* **api:** model claude selalu pakai Opus dari settings.json, bukan deepseek ([9aca45c](https://github.com/asepharyana/zesdex/commit/9aca45cb65d6913d14fecc10d70c180934f69d74))
## [1.19.3](https://github.com/asepharyana/zesdex/compare/v1.19.2...v1.19.3) (2026-08-27)
### Bug Fixes
* **api:** model Opus pakai URL + API custom dari ~/.claude/settings.json ([1f91b44](https://github.com/asepharyana/zesdex/commit/1f91b447080e7106201d77585edb7d38b74e34bb))
## [1.19.2](https://github.com/asepharyana/zesdex/compare/v1.19.1...v1.19.2) (2026-08-27)
### Performance Improvements
* **agent:** stabilkan async & parallel — satu runtime, bounded concurrency, isolasi error ([6a98d52](https://github.com/asepharyana/zesdex/commit/6a98d52d54a69f78710d852a69dda3ac0a4ead31))
## [1.19.1](https://github.com/asepharyana/zesdex/compare/v1.19.0...v1.19.1) (2026-08-27)
### Performance Improvements
* **agent:** rombak alur AI agent — adaptif, hemat token, self-healing ([eac0443](https://github.com/asepharyana/zesdex/commit/eac0443c4c3b8bfcbefd4bad9554168fb6525b94))
# [1.19.0](https://github.com/asepharyana/zesdex/compare/v1.18.4...v1.19.0) (2026-08-27)
### Features
* hapus fitur LSP bawaan (language server protocol) ([93f3c2a](https://github.com/asepharyana/zesdex/commit/93f3c2a3572511b5a84f244980ad71bd1b455e70))
## [1.18.4](https://github.com/asepharyana/zesdex/compare/v1.18.3...v1.18.4) (2026-08-27)
### Performance Improvements
* **tui:** render streaming token secara inkremental + kurangi redraw sia-sia ([2717216](https://github.com/asepharyana/zesdex/commit/271721694beb62e9fa5ff932312b293aa1d56823))
## [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)
-156
View File
@@ -1,156 +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.
---
## Best Practices (Kana Engineering Standards)
This project follows Kana Engineering Best Practices. The following skills are loaded and enforced:
| Skill | Location | Purpose |
|-------|----------|---------|
| `clean-code` | `.claude/skills/clean-code/SKILL.md` | Clean Code principles (naming, functions, classes, comments) |
| `commit-convention` | `.claude/skills/commit-convention/SKILL.md` | Conventional Commits (Bahasa Indonesia) |
| `push-flow-convention` | `.claude/skills/push-flow-convention/SKILL.md` | Pre-commit/pre-push hooks via lefthook |
| `kana-rust-backend-best-practice` | `.claude/skills/kana-rust-backend-best-practice/SKILL.md` | Rust clean-architecture patterns (Axum, SeaORM, etc.) |
### Layering Rules
```
domain/ → application/ → infrastructure/ → interfaces/ → gateway/
(inward) (outward)
```
- **Domain** (Layer 0): Pure entities, value objects, repository/service traits. ZERO external framework deps.
- **Application** (Layer 1): Use-case services (one per file), port traits. Depends ONLY on domain.
- **Infrastructure** (Layer 2): Concrete implementations of domain traits (SQLite, JSON files, LLM clients, LSP servers, MCP).
- **Interfaces** (Layer 3): Presentation adapters — TUI (ratatui), API (Axum), WebSocket, daemon, gRPC, web.
- **Gateway**: Composition root — the only place that wires all layers together.
**Critical:** Domain must NEVER import application, infrastructure, or interfaces. Application must NEVER import infrastructure or interfaces.
### Commit Convention (Bahasa Indonesia)
All commits follow Conventional Commits in Bahasa Indonesia:
```
feat(tool): add batch file delete
fix(ipc): reconnect loop on socket timeout
chore: bump reqwest to 0.13
docs: add architecture diagram to README
refactor(harness): flatten guard pipeline
```
Types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `style`, `perf`, `ci`. All types produce a release (patch minimum). Add `BREAKING CHANGE:` for major bumps.
### Clean Code Principles
- **Functions under ~40 lines**, one level of abstraction, extracted till you drop.
- **No flag arguments** — split `render(true)` into `renderForSuite()` / `renderForSingleTest()`.
- **Command-Query Separation** — function either does or answers, never both.
- **No switch/if-else on type** — replace with factory + polymorphism.
- **No null returns** — use `Option<T>` or empty collections.
- **No magic numbers** — extract named constants.
- **DRY** — no duplication.
- **Tell, Don't Ask** — don't fetch state then decide; tell the object to work.
- **Boy Scout Rule** — leave every module cleaner than you found it.
### Error Handling
- `anyhow::Result` and `anyhow::bail!` throughout (except domain layer typed errors).
- `tracing::warn!` / `tracing::error!` for logging. NEVER stderr (corrupts TUI).
- Never `.unwrap()` or `.expect()` in production code — use `?` or proper error handling.
- Log expected failures at `warn!`, unexpected errors at `error!`.
### Testing
- `#[cfg(test)] mod tests` blocks inline in production files.
- Tests are F.I.R.S.T. — Fast, Independent, Repeatable, Self-validating, Timely.
- Use `Result<()>` as test return type for `?` propagation.
- Mock at boundaries only; prefer fakes for owned abstractions.
### Compiler Bypasses
NEVER use `#[allow(...)]`, `#[expect(...)]`, or `#[allow(dead_code)]`. Fix the underlying code instead.
## 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
-5154
View File
File diff suppressed because it is too large Load Diff
-96
View File
@@ -1,96 +0,0 @@
[workspace]
resolver = "2"
members = [
"apps/domain",
"apps/application",
"apps/infrastructure",
"apps/interfaces/tui",
"apps/interfaces/api",
"apps/interfaces/daemon",
"apps/interfaces/ws",
"apps/interfaces/grpc",
"apps/interfaces/web",
"apps/gateway",
"apps/bootstrap",
]
[workspace.package]
version = "1.18.2"
edition = "2021"
authors = ["asepharyana <superaseph@gmail.com>"]
[workspace.lints.rust]
unused = "deny"
dead_code = "deny"
unreachable_code = "deny"
unused_imports = "deny"
unused_variables = "deny"
unused_mut = "deny"
unused_must_use = "deny"
deprecated = "deny"
trivial_casts = "deny"
trivial_numeric_casts = "deny"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -2 }
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml_ng = "0.10"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "v5"] }
anyhow = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "io-util", "signal"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
reqwest = { version = "0.13", features = ["json", "stream", "blocking", "native-tls-vendored", "form"] }
ratatui = "0.30.2"
crossterm = "0.29"
rusqlite = { version = "0.40", features = ["bundled"] }
thiserror = "1"
base64 = "0.22"
sha2 = "0.11"
hex = "0.4"
libc = "0.2"
dirs = "6"
regex = "1"
globset = "0.4"
ignore = "0.4"
nucleo-matcher = "0.3"
futures-util = "0.3"
rmcp = { version = "2.2", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "macros"] }
lsp-types = "0.97"
tiktoken-rs = "0.12"
similar = "3"
syntect = { version = "5", default-features = false, features = ["default-fancy"] }
pulldown-cmark = { version = "0.13", default-features = false }
infer = "0.19"
webbrowser = "1"
url = "2"
percent-encoding = "2"
dom_smoothie = "0.18.0"
fast_html2md = "0.0.62"
scraper = "0.27.0"
include_dir = "0.7"
axum = { version = "0.8", features = ["macros"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "limit"] }
argon2 = "0.5"
jsonwebtoken = "9"
clap = { version = "4", features = ["derive"] }
rand_core = { version = "0.6", features = ["getrandom"] }
# Clean-architecture workspace crate references
zesdex-domain = { path = "apps/domain" }
zesdex-application = { path = "apps/application" }
zesdex-infrastructure = { path = "apps/infrastructure" }
zesdex-tui = { path = "apps/interfaces/tui" }
zesdex-api = { path = "apps/interfaces/api" }
zesdex-daemon = { path = "apps/interfaces/daemon" }
zesdex-ws = { path = "apps/interfaces/ws" }
zesdex-grpc = { path = "apps/interfaces/grpc" }
zesdex-web = { path = "apps/interfaces/web" }
zesdex-gateway = { path = "apps/gateway" }
zesdex-bootstrap = { path = "apps/bootstrap" }
+13 -14
View File
@@ -1,30 +1,29 @@
# syntax=docker/dockerfile:1
# Zesdex — Multi-stage Docker build
# ===================================
# Stage 1: Build with Rust toolchain
FROM rust:1.85-slim-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libsqlite3-dev && \
rm -rf /var/lib/apt/lists/*
# Zesdex — Multi-stage Docker build (Bun)
# ========================================
# Stage 1: Build with Bun → compile single self-contained CLI binary
FROM oven/bun:1.2 AS builder
WORKDIR /app
COPY . .
COPY bun.lock ./
COPY package.json tsconfig.json ./
COPY src ./src
# Build with release profile (treats warnings as errors via lints)
RUN cargo build --release -p zesdex-backend --bin zesdex
RUN bun install --frozen-lockfile
# Compile a standalone self-contained executable (bundles JS + native addons).
RUN bun build src/interfaces/cli/index.ts --compile --outfile=/app/zesdex
# Stage 2: Minimal runtime image
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libsqlite3-0 && \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/zesdex /usr/local/bin/zesdex
COPY --from=builder /app/zesdex /usr/local/bin/zesdex
ENV ZESDEX_DATA_DIR=/data
VOLUME ["/data"]
ENTRYPOINT ["/usr/local/bin/zesdex"]
-250
View File
@@ -1,250 +0,0 @@
# Zesdex — Autonomous AI Coding Agent
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.
```
┌──────────────────────────────────────────────────────────────┐
│ Mode Selector │
│ TUI (default) ─── Daemon ─── Attach ─── API ─── WS/gRPC/Web │
└──────────────────────────────────────────────────────────────┘
```
---
## Quick Start
```bash
# Run the TUI (default mode)
cargo run
# Run the REST API server
cargo run -- --api --api-port 8080
# Run in daemon mode (background + IPC)
cargo run -- --daemon
# Attach TUI to a running daemon session
cargo run -- --attach <session-id>
# Seed initial data (first run)
cargo run --bin bootstrap
```
### Prerequisites
- **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)
---
## 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
```
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
37 tools across 9 categories:
| 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` |
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>;
}
```
### Hive Mind Orchestration
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.
- **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.
---
## Built-in Features
| 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 |
---
## 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/`):
```
~/.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
```bash
# Build all crates
cargo build
# Run all unit tests (8 tests across 11 crates)
cargo test
# Run clippy linting
cargo clippy --all-targets
# Run with verbose logging
RUST_LOG=debug cargo run
```
### Workspace Crates
| 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 |
### Code Map
Detailed architecture documentation is in `docs/CODEMAPS/`:
| 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 `CHANGELOG.md` for release history.
+201
View File
@@ -0,0 +1,201 @@
# TODO — Rewrite Zesdex Rust → TypeScript/Bun
> Plan lengkap migrasi in-place dari Rust (11 crate, clean architecture 4 lapis) ke monorepo
> TypeScript/Bun. Bahasa Indonesia, commit pakai Conventional Commits.
>
> **Status:** Fase 0–6 **semua selesai**. Rust source dihapus, build CI/Docker/release pakai Bun, `flake.lock` Rust/Nix dibersihkan, wire-shape lama terbaca. Base: `bun run check` bersih, 70 test hijau.
---
## Prinsip & peta pemetaan
| Rust | TS/Bun |
|---|---|
| `serde_json` | `JSON.stringify/parse` + `zod` |
| `chrono` | `Date` / `Date.now()` (epoch ms) |
| `uuid` | `crypto.randomUUID()` |
| `sha2`, `base64 URL_SAFE_NO_PAD` | `node:crypto` / `Buffer.toString('base64url')` |
| `reqwest` | `fetch` / bun HTTP |
| `rusqlite` | `bun:sqlite` |
| `argon2` | `@node-rs/argon2` |
| `jsonwebtoken` | `node:crypto` HMAC HS256 |
| `rmcp` | `@modelcontextprotocol/sdk` |
| `ratatui`/`crossterm` | library TUI Bun / ink |
| `tiktoken-rs` | `@dqbd/tiktoken` (cl100k_base) |
| `clap` | `Bun.argv` / `commander` |
| `tokio` semaphore | Bun async + concurrency pool |
| `libc` PID lock | `node:fs` `O_EXCL` + `process.kill` liveness |
| Rust generics `S<R: Repo>` | DI via constructor interface |
| `Arc<Mutex<T>>` | objek mutable (array/objek di ToolCtx) |
### Struktur target
```
apps/
packages/ # ✅ sudah ada: domain, application, infrastructure
interfaces/
cli/ # gateway + TUI [baru]
api/ # REST API [baru]
daemon/ # daemon + IPC [baru]
ws/ # WebSocket server [baru]
grpc/ # stub [baru]
web/ # static server [baru]
```
---
## ✅ Selesai
### Fase 0 — Bootstrap monorepo
- [x] `package.json` root (workspaces), `tsconfig.json` strict, `.gitignore`
- [x] Paket `domain`, `application`, `infrastructure` (workspace `@zesdex/*`)
- [x] `bun install` + `bun test` jalan
### Fase 1 — Domain layer
- [x] `core`: ChatMessage, Conversation, ChatRequest/Response/StreamEvent, SseParser, ToolCall, repairJson, sanitizeToolArguments, ToolCallResult, UsageStats, Store
- [x] `auth`: Session, SessionId (path-traversal), OAuthToken/Config, repo/service trait
- [x] `cms`: AppConfig, ProviderConfig, ModelRole, Settings+resolveEffectiveModel, SettingsPatch, Memory, EditLog, repo/service trait
- [x] `agent`: Origin, Toast, AgentStatus, TurnEvent, SessionRuntime, AgentTurnParams, AgentProgress, ExecutionModel
- [x] `workflow`/`subagent`: WorkflowScript/NodeDirective, AccessTier, prompt builders
- [x] 27 test domain
### Fase 2 — Application layer
- [x] `ports`: ProviderService, PasswordService, TokenService, AuthService
- [x] `AgentTurnServiceImpl` (loop 50 iter, auto-compact 60k, adaptive token, temp, ErrorTracker, bounded parallel)
- [x] `OAuthUseCase` (PKCE S256 + CSRF state)
- [x] `SessionServiceImpl`, `MemoryServiceImpl`, `SettingsServiceImpl`, `ConversationServiceImpl`
- [x] 11 test application
### Fase 3a — LLM client
- [x] `LlmClient` (fetch, retry 10x backoff+jitter, abort 401/402/403)
- [x] `resolve_api_key`, `is_auth_error`, `backoff_for_error`
### Fase 3b — Tool system (`commit a291dd1`)
- [x] `Tool` interface, `ToolCtx`/`ToolCtxBuilder`, `InfrastructureToolExecutor`
- [x] Registry: `allTools` (37), `toolDefs`, `toolIsRisky`, `toolIsParallelSafe`
- [x] FS: read, write, edit, delete · **Guard**: graduated checks, resolve_path sandbox
- [x] Search: grep, glob (matcher ringan)
- [x] Shell: bash (timeout), bash_output, bash_kill · **Filter**: checkGitDestructive, checkCredentialRead
- [x] Git: git_operator, git_worktree, git_cred
- [x] Memory: remember, forget, recall
- [x] Util: cd, dir_list, dir_cache_update, pong, todowrite, todofinish
- [x] Reasoning/Plan: sequential_think, plan_enter, plan_ready
- [x] Web: web_search (async → SearXNG)
- [x] Semantic: semantic_search, rebuild_index, list_symbols (index multi-bahasa)
- [x] Best-practice: best_practice, commit_convention + BestPracticeEngine
- [x] Agent/Workflow: spawn_agents, spawn_pipeline, parallel_delegate, workflow_run, note_finding, read_findings, hive_mind (delegasi ke placeholder 3c/3d)
- [x] 16 test tool system
### Fase 3e — Persistence
- [x] File repos: settings, app_config (+ auto-detect kredensial Claude), conversation, edits.jsonl, markdown memory, rewind blob, session, oauth, session lock
- [x] `writeJsonAtomic`, `slugify`, `buildWorkspaceTree`, `runCommand`
### Fase 3c — Subagent engine
**Sumber Rust:** `apps/infrastructure/src/subagent/{engine,division,context,provider,spawn}.rs`, `subagent/auto/*`
- [x] `AccessTier.toolsFor(access)` — `tools/division.ts`: Read / Write / Full yang mem-filter registry
- [x] `run_agent` loop (`tools/engine.ts`): `MAX_ITERATIONS=25`, `TOOL_OUTPUT_MAX_CHARS=12_000`,
`MAX_CONSECUTIVE_TOOL_ERRORS=3`, `MAX_PARALLEL_TOOLS=8`; recovery note setelah 3 error beruntun
- [x] `SubagentContext` (`subagent/context.ts`) — directive, toolCtx, access, baseURL, apiKey, model
- [x] `spawn_subagent` — `spawn_tools.ts` — runAgent berbasis HTTP provider + config_resolver
- [x] `resolve_subagent_provider` (`subagent/provider.ts`) — pilih provider/model subagent dari settings + app_config
- [x] `spawn_tools.ts` — isi body `spawnAgents` (paralel) & `spawnPipeline` (berurutan)
- [x] `delegate.ts` — `runParallelDelegation` untuk `parallel_delegate`
- [x] `http_provider.ts` — shared HTTP provider service builder
- [ ] Auto-reviewer (`subagent/auto/*`) — `spawn_background_review`: git diff → LLM review → perbaiki;
wiring `review_enabled` (yang sekarang no-op di `Review` agent)
- [x] Placeholder di `apps/packages/infrastructure/src/subagent/*` diisi penuh
### Fase 3d — Workflow + hive-mind
**Sumber Rust:** `apps/infrastructure/src/workflow/{script,mod,docs}.rs`, `workflow/engine/{execution,phases,primitives}.rs`, `workflow/hive_mind/{complexity,cycle,synthesis,live,types}.rs`
- [x] `parse_workflow_script(yaml)` — parser YAML workflow (`workflow/script.ts`)
- [x] `execute_workflow(script, ctx)` — eksekusi multi-phase (`workflow/engine.ts` + `workflow/orchestrate.ts`)
- [x] Phase primitives: run sub-agent per node (`executePrimitive`)
- [x] `execute_cycle(cycle, ctx)` — hive-mind cycle, concurrency 8, kumpulkan NodeOutput (`workflow/cycle.ts`)
- [x] `synthesize_consensus(node_outputs, ctx)` — konsensus via LLM + fallback concat (`workflow/synthesis.ts`)
- [x] `complexity_heuristic` — pilih apakah cukup 1 cycle / butuh banyak (`workflow/complexity.ts`)
- [x] `docs.ts` — tulis convergence/decision doc (`workflow/docs.ts`)
- [x] Isi body `WorkflowRun` & `HiveMind` tool (ganti placeholder) di `tools/workflow.ts`
- [x] `orchestrate.ts` — top-level `executeWorkflow` + `executeHiveMind` entry points
---
## ⬜ Yang belum dikerjakan
### Fase 3f — Auth infrastructure
**Sumber Rust:** `apps/infrastructure/src/auth/{password,jwt,oauth_loopback,mod}.rs`
- [x] `password.ts` — Argon2 via `@node-rs/argon2` (hash + verify), port `PasswordService`
- [x] `jwt.ts` — HMAC HS256 via `node:crypto` (access + refresh token, typ, exp), port `TokenService`
- [x] `oauth_loopback.ts` — server TCP loopback lokal utk terima redirect OAuth; port `oauth_loopback`
- [x] `mod.ts` (`index.ts`) — composisi ke `AuthService`
- [x] Tambah dep `@node-rs/argon2` di `infrastructure/package.json`
### Fase 3g — IPC + bgbash
**Sumber Rust:** `apps/infrastructure/src/ipc/{frame,protocol,server,client,conn}.rs`, `apps/infrastructure/src/bgbash/{job,control}.rs`
- [x] `ipc/frame.ts` — length-prefixed framing via `node:net` (kirim/terima bingkai biner)
- [x] `ipc/conn.ts` — `Connection` framed JSON socket; `ipc/protocol.ts` — wire types
- [x] `ipc/server.ts` + `ipc/client.ts` — Unix socket server + attach client; protocol message (NLJSON/binary)
- [x] `bgbash/job.ts` — `BashJob` (pid, output file, log), `spawnBashJob`
- [x] `bgbash/control.ts` — registry job global: `register`, `cancel(job_id)`, `is_running`
- [x] Wire `bash` tool `run_in_background` → `spawnBashJob` + `register`; `bash_kill` → `cancel`
- [x] Update `bash_output` untuk baca dari registry/output dir
### Fase 4 — Gateway + CLI + TUI
**Sumber Rust:** `apps/gateway/src/{lib,main}.rs`, `apps/interfaces/tui/src/**`
- [x] `apps/interfaces/cli/` (paket Bun):
- [x] Parser arg: `--api/--daemon/--attach/--ws/--grpc/--web`, dispatch mode (`args.ts`)
- [x] Logging ke file (`index.ts`)
- [x] `run_single_process` — komposisi root: wire semua implementasi konkret sekali (`compose.ts`) + REPL loop
- [x] Bootstrap seed (`bootstrap.ts`) — wire-shape settings lama terbaca
- [x] CLI: parser arg + dispatch + single-process REPL loop + bootstrap seed (Smoke: `bun run cli --help`, `bun run bootstrap` jalan; loop turn + tool read/write)
- [x] TUI mode (`@zesdex/tui`, tanpa ncurses — render garis ANSI):
- [x] Event loop raw-mode stdin (run.ts) + key decode (decodeKey)
- [x] Key dispatcher `handleKey` → `Action[]` + overlay/autocomplete/editor handling (controller.ts)
- [x] `Action` enum + `applyAction` dispatcher (action.ts) + slash-command parse/apply (command.ts)
- [x] `InputState` (buffer/cursor/history/autocomplete) + `AppStateRest` state machine (state.ts)
- [x] Renderer ringan (render.ts): transcript, input, overlay, status — 20 unit tests hijau
- [x] Bootstrap seed (`bootstrap`) — `bun run bootstrap` seed defaults, wire-shape settings lama terbaca
### Fase 5 — Server interface: api / daemon / ws / grpc / web
**Sumber Rust:** `apps/interfaces/{api,daemon,ws,grpc,web}/src/**`, `apps/infrastructure/src/middleware/*`
- [x] **API** (`apps/interfaces/api/`):
- [x] Endpoint: `auth` (login/register/refresh, rate-limited), `sessions` CRUD, `conversations`, `chat/completions` proxy, `health`
- [x] JWT middleware, CORS
- [x] DTO (dto/)
- [x] Smoke `bun run api`: health 200; register/login 200; wrong-pwd 401; protected-no-token 401; create/list/delete session; add conversation msg
- [x] **daemon** — IPC agent-driver loop (Submit/Close) over per-session Unix socket; streaming token frames; wire CLI `--daemon`/`--attach`
- [x] **ws** — handler `/ws`, protokol token/connected/done/error; smoke: no-token rejected 401, with-token welcome
- [x] **grpc** — stub (health endpoint); smoke: `/grpc/health` 200
- [x] **web** — static server + proteksi traversal; smoke: `/` serves index, traversal 404
- [x] CLI `--api`/`--ws`/`--grpc`/`--web` dispatch → start server masing-masing (smoke: `bun run cli --api --api-port` → health 200)
### Fase 6 — Bersih-bersih + verifikasi akhir
- [x] Hapus semua sisa crate Rust: `apps/{domain,application,infrastructure,gateway,bootstrap,interfaces/*}` file `.rs`, `Cargo.toml`, `Cargo.lock`, `.cargo/`, `lefthook.yml`
- [x] Update `install.sh` → Bun
- [x] Update CI workflow: `.github/workflows/{ci,release}.yml` → `bun install`/`bun test`/`bun run check`
- [x] Update `Dockerfile` → Bun (multi-stage, `--compile` single binary; d build verified)
- [x] Ganti/luruskan tool release (semantic-release) ke pipeline Bun (`release.yml` test job → bun; prepareCmd → `bun run check`)
- [x] `flake.lock` dead (Rust/Nix era) dihapus — deploy infra 100% Bun (`deploy.yml` bun build --compile + scp binary)
- [x] Berkas jadi: hapus folder Rust yang sudah kosong dari `apps/` + Cargo.toml/lock + .cargo + lefthook
- [x] Verifikasi akhir: `tsc --noEmit` exit 0, `bun test` 74 hijau, `bun run cli --help` jalan
- [x] Wire-shape lama terbaca — `settings.json` (Rust-era shape: api_keys/provider/model/review_*) dimuat true di headless + TUI; repos direct JSON parser. Verified: `--headless "1+1"` → "2", TUI header tunjuk `claude-opus-5`.
---
## Verifikasi global (tiap fase)
- [ ] `bun run build` / `tsc --noEmit` tanpa error (strict)
- [ ] `bun test` — port test Rust ke `*.test.ts` (SseParser, repairJson, resolveEffectiveModel, SessionId, turn_service, tool system, subagent, workflow)
- [ ] Smoke: `bun run cli --help`; agent mode TUI dengan model stub/lokal; pastikan loop turn + tool read/write
- [ ] Server: `bun run api` + hit health/auth; `bun run ws` + connect
- [ ] Commit tiap fase dengan message Bahasa Indonesia + Conventional Commits; test hijau sebelum lanjut
## Catatan
- Registry tool = **38** (terhitung workflow_run, note_finding, read_findings, hive_mind; ExploreCodebase tidak diport).
- `Tool.run` boleh return `string | Promise<string>`.
- `spawn_agents`/`spawn_pipeline`/`parallel_delegate` sudah terisi penuh via 3c.
- Workflow/hive-mind tools sudah terisi penuh via 3d.
-21
View File
@@ -1,21 +0,0 @@
[package]
name = "zesdex-application"
version.workspace = true
edition.workspace = true
authors.workspace = true
# Application layer — port traits (interfaces), use cases, DTOs.
# Depends ONLY on domain. Application services orchestrate domain objects
# through port traits without knowing concrete implementations.
[dependencies]
zesdex-domain = { path = "../domain" }
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
uuid.workspace = true
anyhow.workspace = true
tracing.workspace = true
tokio.workspace = true
base64.workspace = true
sha2.workspace = true
url.workspace = true
-57
View File
@@ -1,57 +0,0 @@
//! 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>>;
}
-29
View File
@@ -1,29 +0,0 @@
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};
-377
View File
@@ -1,377 +0,0 @@
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 crate::ports::ProviderService;
use super::{ExploreService, ToolExecutor};
/// 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(
&params.turn_events,
TurnEvent::SystemNote {
kind: "info".into(),
message: "🔍 Exploring codebase structure...".into(),
},
);
match explorer.explore(&user_query, &workspace_root, &params.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(
&params.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(
&params.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(&params.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(&params.messages, &params.abort, &params.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(
&params.turn_events,
TurnEvent::StreamDone(assistant_msg.clone()),
);
emit_usage(&params.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(), &params.turn_events, tc).await;
params
.messages
.push(ChatMessage::tool(tc.id.clone(), output));
}
}
Err(e) => {
warn!("{e}");
push_event(
&params.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(
&params.turn_events,
TurnEvent::Compacted(compacted),
);
push_event(&params.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(())
}
}
}
-16
View File
@@ -1,16 +0,0 @@
//! Auth use-case implementations.
//!
//! Contains concrete service types that implement the domain's
//! authentication and session management traits by coordinating
//! injected repository and port dependencies.
//!
//! # Use Cases
//!
//! - [`oauth_service`] — `OAuthUseCase`: OAuth 2.0 authorization-code + PKCE flow
//! - [`session_service`] — `SessionServiceImpl`: session CRUD lifecycle
pub mod oauth_service;
pub mod session_service;
pub use oauth_service::{OAuthFlowStore, OAuthUseCase, TokenExchanger};
pub use session_service::SessionServiceImpl;
-250
View File
@@ -1,250 +0,0 @@
//! OAuth 2.0 authorization-code + PKCE flow use-case.
//!
//! `OAuthUseCase` orchestrates the standard PKCE-enhanced OAuth flow:
//!
//! 1. **`start_flow`** — generates a cryptographic PKCE code verifier,
//! derives its S256 challenge, creates a CSRF state token, persists
//! the verifier + state via `OAuthFlowStore`, and builds an
//! authorization URL with all required parameters.
//! 2. **`complete_flow`** — validates the returned `state` against the
//! stored value (CSRF check), reads the stored verifier, delegates
//! the token-code exchange to an injected `TokenExchanger`, and
//! persists the resulting `OAuthToken` via `OAuthRepository`.
//! 3. **`get_token`** — loads the stored OAuth token (if any).
//!
//! # Portability
//!
//! The service is generic over three injected dependencies:
//! - `R: OAuthRepository` — token persistence
//! - `S: OAuthFlowStore` — ephemeral flow state (verifier + CSRF state)
//! - `E: TokenExchanger` — the HTTP token-endpoint exchange
//!
//! This keeps all I/O and protocol-level concerns abstracted behind
//! port traits; the service itself contains only orchestration logic.
use std::path::PathBuf;
use tracing;
use zesdex_domain::auth::{OAuthConfig, OAuthRepository, OAuthToken, ServiceError};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use sha2::{Digest, Sha256};
// ---------------------------------------------------------------------------
// Port traits (defined here because they are specific to this use-case)
// ---------------------------------------------------------------------------
/// Persistence contract for ephemeral OAuth flow state.
///
/// Between `start_flow` and `complete_flow` the verifier and CSRF state
/// must survive across process boundaries (the user opens a browser, the
/// provider redirects back to a loopback listener on the next invocation).
///
/// Implementors store key-value pairs to disk or another durable medium
/// 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>;
/// Load the stored PKCE code verifier.
fn load_verifier(&self) -> Result<String, ServiceError>;
/// Load the stored CSRF state token.
fn load_state(&self) -> Result<String, ServiceError>;
/// Clear stored flow state (verifier + state).
fn clear(&self) -> Result<(), ServiceError>;
}
/// Abstraction for exchanging an authorization code for tokens.
///
/// Implementors handle the HTTP POST to the provider's token endpoint
/// with the appropriate form-encoded parameters, parse the JSON
/// response, and return the extracted `OAuthToken`.
pub trait TokenExchanger: Send + Sync {
/// Exchange an authorization code for an access token.
///
/// ## Parameters
/// - `token_url` — the provider's token endpoint URL
/// - `client_id` — OAuth client identifier
/// - `client_secret` — optional client secret
/// - `redirect_uri` — must match the URI used in `start_flow`
/// - `code` — the authorization code from the provider's redirect
/// - `code_verifier` — the PKCE verifier from `start_flow`
fn exchange_code(
&self,
token_url: &str,
client_id: &str,
client_secret: Option<&str>,
redirect_uri: &str,
code: &str,
code_verifier: &str,
) -> Result<OAuthToken, ServiceError>;
}
// ---------------------------------------------------------------------------
// PKCE helpers
// ---------------------------------------------------------------------------
/// Generate a PKCE code-verifier and its S256 code-challenge.
///
/// Uses 32 cryptographically random bytes, base64url-encoded (no padding)
/// for the verifier, then SHA-256 hashes the verifier and base64url-encodes
/// the digest for the challenge. This satisfies the PKCE `S256` method
/// which requires a minimum verifier length of 43 characters.
fn generate_pkce_pair() -> (String, String) {
// 32 random bytes → 43 base64url chars (well above the 43-char PKCE
// minimum).
let mut bytes = [0u8; 32];
bytes[..16].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
bytes[16..].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
let verifier = URL_SAFE_NO_PAD.encode(bytes);
let challenge = {
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
URL_SAFE_NO_PAD.encode(hasher.finalize())
};
(verifier, challenge)
}
/// Generate a random CSRF state token (UUID-based, 36 chars).
fn generate_state_token() -> String {
uuid::Uuid::new_v4().to_string()
}
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
/// Concrete OAuth flow use-case.
///
/// Generic over three dependencies:
/// - `R` — token persistence (`OAuthRepository`)
/// - `S` — flow-state persistence (`OAuthFlowStore`)
/// - `E` — token-endpoint HTTP exchange (`TokenExchanger`)
pub struct OAuthUseCase<R, S, E> {
/// Repository for persisting / loading OAuth tokens.
pub token_repo: R,
/// Store for ephemeral flow state (verifier + CSRF state).
pub flow_store: S,
/// Token-endpoint HTTP exchanger.
pub token_exchanger: E,
/// File path for the token JSON file.
pub token_path: PathBuf,
}
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 {
OAuthUseCase {
token_repo,
flow_store,
token_exchanger,
token_path,
}
}
}
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
zesdex_domain::auth::OAuthService for OAuthUseCase<R, S, E>
{
fn start_flow(
&self,
config: &OAuthConfig,
redirect_uri: &str,
) -> Result<(String, String), ServiceError> {
if config.auth_url.is_empty() {
return Err(ServiceError::InvalidConfig(
"OAuth auth_url is empty".to_string(),
));
}
let (verifier, challenge) = generate_pkce_pair();
let state = generate_state_token();
// Persist verifier + state so `complete_flow` can retrieve them.
self.flow_store.save_flow_state(&verifier, &state)?;
tracing::debug!(
auth_url = %config.auth_url,
redirect_uri = %redirect_uri,
state_len = state.len(),
"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
))
})?;
url.query_pairs_mut()
.append_pair("response_type", "code")
.append_pair("client_id", &config.client_id)
.append_pair("redirect_uri", redirect_uri)
.append_pair("scope", &config.scopes.join(" "))
.append_pair("state", &state)
.append_pair("code_challenge_method", "S256")
.append_pair("code_challenge", &challenge);
Ok((url.to_string(), state))
}
fn complete_flow(
&self,
config: &OAuthConfig,
redirect_uri: &str,
code: &str,
state: &str,
) -> Result<OAuthToken, ServiceError> {
// CSRF check: validate the returned state against the stored value.
let expected_state = self.flow_store.load_state()?;
if expected_state != state {
return Err(ServiceError::StateMismatch);
}
// Read the PKCE verifier that was saved in `start_flow`.
let verifier = self.flow_store.load_verifier()?;
tracing::debug!(
token_url = %config.token_url,
code_len = code.len(),
"completing OAuth flow — exchanging code for token",
);
// Delegate the HTTP token exchange to the injected exchanger.
let token = self.token_exchanger.exchange_code(
&config.token_url,
&config.client_id,
config.client_secret.as_deref(),
redirect_uri,
code,
&verifier,
)?;
// Persist the token and clean up flow state.
self.token_repo.save_token(&self.token_path, &token)?;
let _ = self.flow_store.clear();
Ok(token)
}
fn get_token(&self) -> Result<Option<OAuthToken>, ServiceError> {
self.token_repo
.load_token(&self.token_path)
.map_err(ServiceError::Repository)
}
}
@@ -1,89 +0,0 @@
//! Session management use-case.
//!
//! `SessionServiceImpl` implements [`SessionService`] from the domain
//! layer by delegating CRUD operations to injected repository traits.
//!
//! # Flow
//!
//! - **`create_session`** — generates a UUID v4 id, creates a `Session`
//! entity with the given title, persists via `SessionRepository`.
//! - **`list_all`** — delegates to `SessionRepository::list_sessions`.
//! - **`archive_session`** — loads session, sets `archived = true`,
//! persists the updated entity.
//!
//! # Generics
//!
//! - `R: SessionRepository` — session CRUD persistence
//! - `L: SessionLockRepository` — session lock acquire/release
use std::path::PathBuf;
use tracing;
use uuid::Uuid;
use zesdex_domain::auth::{
ServiceError, Session, SessionId, SessionLockRepository, SessionRepository,
};
/// Concrete session service backed by injected repository implementations.
pub struct SessionServiceImpl<R: SessionRepository, L: SessionLockRepository> {
/// Repository for session CRUD operations.
pub session_repo: R,
/// Repository for session lock acquire/release.
pub lock_repo: L,
/// Base data directory passed to repository methods.
pub base_dir: PathBuf,
}
impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
/// Create a new session service with the given repositories and base
/// data directory.
pub fn new(session_repo: R, lock_repo: L, base_dir: PathBuf) -> Self {
SessionServiceImpl {
session_repo,
lock_repo,
base_dir,
}
}
}
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 title_owned = if title.is_empty() {
"New Session".to_string()
} else {
title.to_string()
};
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)?;
Ok(session)
}
fn list_all(&self) -> Result<Vec<Session>, ServiceError> {
tracing::debug!("listing all sessions");
self.session_repo
.list_sessions(&self.base_dir)
.map_err(ServiceError::Repository)
}
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)?;
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)?;
Ok(())
}
}
@@ -1,72 +0,0 @@
//! Conversation use-case implementation.
//!
//! `ConversationServiceImpl` implements [`ConversationService`] from the
//! domain layer. It is generic over `R: ConversationRepository`, delegating
//! all persistence to that adapter.
//!
//! # Flow
//!
//! Each method computes the session directory from the session ID, then
//! delegates the actual I/O to the injected `repo`. Error context is
//! added at this layer to identify which session caused the failure.
use std::path::PathBuf;
use tracing;
use zesdex_domain::cms::{Conversation, ConversationRepository, ServiceError};
use zesdex_domain::core::ChatMessage;
/// Service implementation for conversation CRUD operations.
///
/// Generic over `R: ConversationRepository` so the persistence layer
/// can be swapped without changing business logic.
pub struct ConversationServiceImpl<R> {
pub repo: R,
/// Base directory containing session subdirectories.
pub sessions_dir: PathBuf,
}
impl<R: ConversationRepository> ConversationServiceImpl<R> {
/// Create a new service with the given repository and sessions directory.
pub fn new(repo: R, sessions_dir: impl Into<PathBuf>) -> Self {
tracing::debug!("creating ConversationServiceImpl");
Self {
repo,
sessions_dir: sessions_dir.into(),
}
}
/// Compute the session directory for a given session id.
fn session_dir(&self, session_id: &str) -> PathBuf {
self.sessions_dir.join(session_id)
}
}
impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
for ConversationServiceImpl<R>
{
fn load_conversation(&self, session_id: &str) -> Result<Conversation, ServiceError> {
tracing::debug!("loading conversation for session {session_id}");
let dir = self.session_dir(session_id);
self.repo.load(&dir).map_err(ServiceError::Repository)
}
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError> {
tracing::debug!("saving conversation for session {}", conv.session_id);
let dir = self.session_dir(&conv.session_id);
self.repo.save(&dir, conv)?;
Ok(())
}
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);
self.repo.save(&dir, conv)?;
Ok(())
}
}
@@ -1,59 +0,0 @@
//! Memory use-case implementation.
//!
//! `MemoryServiceImpl` implements [`MemoryService`] from the domain
//! layer. It is generic over `R: MemoryRepository`, delegating all
//! persistence to that adapter.
//!
//! # Flow
//!
//! Each method delegates to the injected `repo` with the configured
//! `memory_dir`. Error context is added at this layer to identify which
//! memory operation failed.
use std::path::PathBuf;
use tracing;
use zesdex_domain::cms::{Memory, MemoryRepository, ServiceError};
/// Service implementation for memory CRUD operations.
///
/// Generic over `R: MemoryRepository` so the persistence layer can be
/// swapped without changing business logic.
pub struct MemoryServiceImpl<R> {
pub repo: R,
/// Base directory for memory storage files.
pub memory_dir: PathBuf,
}
impl<R: MemoryRepository> MemoryServiceImpl<R> {
/// Create a new service with the given repository and memory directory.
pub fn new(repo: R, memory_dir: impl Into<PathBuf>) -> Self {
tracing::debug!("creating MemoryServiceImpl");
Self {
repo,
memory_dir: memory_dir.into(),
}
}
}
impl<R: MemoryRepository> zesdex_domain::cms::MemoryService for MemoryServiceImpl<R> {
fn list_memories(&self) -> Result<Vec<String>, ServiceError> {
tracing::debug!("listing memories from {:?}", self.memory_dir);
self.repo
.list(&self.memory_dir)
.map_err(ServiceError::Repository)
}
fn save_memory(&self, memory: &Memory) -> Result<(), ServiceError> {
tracing::debug!("saving memory '{}'", memory.name);
self.repo.save(&self.memory_dir, memory)?;
Ok(())
}
fn delete_memory(&self, name: &str) -> Result<(), ServiceError> {
tracing::debug!("deleting memory '{name}'");
self.repo
.delete(&self.memory_dir, name)
.map_err(ServiceError::Repository)
}
}
-18
View File
@@ -1,18 +0,0 @@
//! CMS use-case implementations.
//!
//! Contains concrete service types that implement the domain's CMS
//! service traits by coordinating injected repository dependencies.
//!
//! # Use Cases
//!
//! - [`conversation_service`] — `ConversationServiceImpl`: conversation CRUD
//! - [`memory_service`] — `MemoryServiceImpl`: long-term memory management
//! - [`settings_service`] — `SettingsServiceImpl`: settings & app-config management
pub mod conversation_service;
pub mod memory_service;
pub mod settings_service;
pub use conversation_service::ConversationServiceImpl;
pub use memory_service::MemoryServiceImpl;
pub use settings_service::SettingsServiceImpl;
@@ -1,76 +0,0 @@
//! Settings and app-config use-case implementation.
//!
//! `SettingsServiceImpl` implements [`SettingsService`] from the domain
//! layer. It is generic over `S: SettingsRepository` and `C: AppConfigRepository`,
//! delegating persistence to those adapters.
//!
//! # Flow
//!
//! Each method delegates to the appropriate injected repository with the
//! configured `base_dir`. The `update_provider` method coordinates between
//! both repositories: load app config → mutate provider map → save app config.
use std::path::PathBuf;
use tracing;
use zesdex_domain::cms::{
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings,
SettingsRepository,
};
/// Service implementation for settings and app-config operations.
///
/// Generic over `S: SettingsRepository` and `C: AppConfigRepository` so
/// the persistence layer can be swapped without changing business logic.
pub struct SettingsServiceImpl<S, C> {
pub settings_repo: S,
pub app_config_repo: C,
pub base_dir: PathBuf,
}
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 {
tracing::debug!("creating SettingsServiceImpl");
Self {
settings_repo,
app_config_repo,
base_dir: base_dir.into(),
}
}
}
impl<S: SettingsRepository, C: AppConfigRepository>
zesdex_domain::cms::SettingsService for SettingsServiceImpl<S, C>
{
fn load_settings(&self) -> Result<Settings, ServiceError> {
tracing::debug!("loading settings");
self.settings_repo
.load(&self.base_dir)
.map_err(ServiceError::Repository)
}
fn save_settings(&self, settings: &Settings) -> Result<(), ServiceError> {
tracing::debug!("saving settings");
self.settings_repo.save(&self.base_dir, settings)?;
Ok(())
}
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
.providers
.insert(name.to_string(), config.clone());
self.app_config_repo.save(&self.base_dir, &app_config)?;
Ok(())
}
}
-57
View File
@@ -1,57 +0,0 @@
//! # Zesdex Application Layer
//!
//! Defines port traits (interfaces) and use-case implementations for the
//! Zesdex application. This crate depends **only** on the domain crate;
//! it has no knowledge of infrastructure or interface adapters.
//!
//! ## Architecture
//!
//! ```text
//! apps/application/src/
//! ├── lib.rs — crate root, re-exports
//! ├── ports/ — Port traits (interfaces to external services)
//! │ ├── provider.rs -- ProviderService (LLM chat completion)
//! │ ├── password.rs -- PasswordService (hash / verify)
//! │ ├── token.rs -- TokenService (JWT create / verify)
//! │ └── authentication.rs -- AuthService (combined auth)
//! ├── auth/ — Auth use-cases
//! │ ├── oauth_service.rs -- OAuth 2.0 PKCE flow
//! │ └── session_service.rs -- Session CRUD lifecycle
//! └── cms/ — CMS use-cases
//! ├── conversation_service.rs -- Conversation CRUD
//! ├── memory_service.rs -- Long-term memory management
//! └── settings_service.rs -- Settings & app-config management
//! ```
//!
//! ## Key Design Principle
//!
//! Application services are generic over their repository/port dependencies.
//! Concrete implementations are injected at the composition root, keeping
//! the use-case logic independent of any specific persistence or infrastructure
//! technology.
pub mod auth;
pub mod cms;
pub mod ports;
pub mod agent;
// Re-export port traits for ergonomic access.
pub use ports::*;
// Re-export auth use-cases.
pub use auth::{
oauth_service::{OAuthFlowStore, OAuthUseCase, TokenExchanger},
session_service::SessionServiceImpl,
};
// Re-export CMS use-cases.
pub use cms::{
conversation_service::ConversationServiceImpl,
memory_service::MemoryServiceImpl,
settings_service::SettingsServiceImpl,
};
pub use agent::{
AgentTurnService, ExploreOutput, ExploreService, ToolExecutor,
turn_service::{AgentTurnServiceImpl, compact_messages_with_ai},
};
@@ -1,35 +0,0 @@
//! AuthService port — combined authentication operations.
//!
//! Defines a high-level authentication trait that composes password
//! verification and token generation into a single use-case boundary.
//! Implementations delegate to the injected `PasswordService` and
//! `TokenService` adapters.
use anyhow::Result;
use std::future::Future;
/// High-level authentication service combining password verification
/// and token issuance (login flow).
///
/// # Flow
///
/// 1. **`authenticate`** — verify a subject's password against a stored hash.
/// 2. **`issue_tokens`** — generate an access + refresh token pair for a subject.
///
/// Implementations are generic over `PasswordService` and `TokenService`
/// port traits.
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;
/// Issue a new access + refresh token pair for the given subject.
///
/// Returns `(access_token, refresh_token)`.
fn issue_tokens(&self, sub: &str) -> Result<(String, String)>;
}
-22
View File
@@ -1,22 +0,0 @@
//! Port traits — interfaces for external / infrastructure services.
//!
//! These traits define the boundaries between the application layer and
//! the outside world. Infrastructure adapters implement these traits;
//! the application layer depends only on the trait definitions.
//!
//! # Ports
//!
//! - [`provider`] — `ProviderService`: LLM chat completion (streaming + non-streaming)
//! - [`password`] — `PasswordService`: password hashing and verification
//! - [`token`] — `TokenService`: JWT access/refresh token generation and verification
//! - [`authentication`] — `AuthService`: combined authentication operations
pub mod authentication;
pub mod password;
pub mod provider;
pub mod token;
pub use authentication::AuthService;
pub use password::PasswordService;
pub use provider::ProviderService;
pub use token::TokenService;
-24
View File
@@ -1,24 +0,0 @@
//! PasswordService port — password hashing and verification abstraction.
//!
//! Defines the trait that password-hashing adapters (argon2, bcrypt, etc.)
//! implement. The application layer depends only on this trait, never on
//! a concrete hashing library.
use anyhow::Result;
use std::future::Future;
/// Abstraction for password hashing and verification.
///
/// Implementors handle the actual hashing algorithm (argon2, bcrypt, etc.)
/// and parameter selection. The trait is `Send + Sync` for use in async
/// service layers.
pub trait PasswordService: Send + Sync {
/// Hash a plaintext password and return the encoded hash string
/// (suitable for storage in a credential store).
fn hash(&self, password: &str) -> impl Future<Output = Result<String>> + Send;
/// Verify a plaintext password against a previously-hashed string.
///
/// Returns `true` if the password matches the hash, `false` otherwise.
fn verify(&self, password: &str, hash: &str) -> impl Future<Output = Result<bool>> + Send;
}
-56
View File
@@ -1,56 +0,0 @@
//! ProviderService port — LLM chat completion provider abstraction.
//!
//! Defines the trait that HTTP-based provider clients (OpenAI, Anthropic,
//! etc.) implement. Supports both non-streaming and SSE-streaming chat
//! completion requests.
//!
//! # Flow
//!
//! 1. Caller builds a message list and optional tool definitions.
//! 2. `chat` sends a non-streaming request and returns the full response.
//! 3. `chat_stream` sends a streaming request and invokes `on_event` for
//! each parsed `StreamEvent` as it arrives, then returns the assembled
//! message and usage.
use anyhow::Result;
use std::future::Future;
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
/// Abstraction for an LLM provider chat-completion service.
///
/// Both methods accept a message list, optional tool definitions, and
/// generation parameters. Implementors handle authentication, HTTP
/// transport, retry logic, and response parsing internally.
///
/// # Send + Sync
///
/// This trait is `Send + Sync` so it can be shared across async tasks
/// and injected into service structs that require thread safety.
pub trait ProviderService: Send + Sync {
/// Send a non-streaming chat completion request.
///
/// Returns the assistant's `ChatMessage` and optional token usage
/// `(prompt_tokens, completion_tokens)`.
fn chat(
&self,
messages: &[ChatMessage],
tools: Option<Vec<ToolDef>>,
max_tokens: Option<u32>,
temperature: Option<f32>,
) -> impl Future<Output = Result<(ChatMessage, Option<(u64, u64)>)>> + Send;
/// Send a streaming chat completion request.
///
/// `on_event` is called for every parsed SSE event and returns `false`
/// to signal abort (caller cancellation). Returns the fully assembled
/// assistant message and optional usage once the stream completes.
fn chat_stream(
&self,
messages: &[ChatMessage],
tools: Option<Vec<ToolDef>>,
max_tokens: Option<u32>,
temperature: Option<f32>,
on_event: Box<dyn FnMut(&StreamEvent) -> bool + Send>,
) -> impl Future<Output = Result<(ChatMessage, Option<(u64, u64)>)>> + Send;
}
-32
View File
@@ -1,32 +0,0 @@
//! TokenService port — JWT access and refresh token abstraction.
//!
//! Defines the trait that JWT adapter implementations provide. Covers
//! token generation (pair of access + refresh tokens) and access token
//! verification (returns the subject claim).
use anyhow::Result;
/// Abstraction for JWT-based token generation and verification.
///
/// Implementors handle signing key management, token serialisation,
/// and expiry validation. The trait is `Send + Sync` for use across
/// thread boundaries.
pub trait TokenService: Send + Sync {
/// Generate an access + refresh token pair for the given subject
/// identifier.
///
/// Returns `(access_token, refresh_token)`.
fn generate_tokens(&self, sub: &str) -> Result<(String, String)>;
/// Verify an access token and return the embedded subject claim.
///
/// 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>;
}
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "zesdex-bootstrap"
version.workspace = true
edition.workspace = true
authors.workspace = true
# Bootstrap binary — seeds initial system data (permissions, roles,
# admin user) idempotently. Run once after first deployment.
[[bin]]
name = "bootstrap"
path = "src/main.rs"
[dependencies]
zesdex-domain = { path = "../domain" }
zesdex-application = { path = "../application" }
zesdex-infrastructure = { path = "../infrastructure" }
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
uuid.workspace = true
anyhow.workspace = true
tokio.workspace = true
tracing.workspace = true
dirs.workspace = true
-2
View File
@@ -1,2 +0,0 @@
//! Bootstrap library — shared utilities for the bootstrap binary.
//! The main entry point is in `main.rs`.
-44
View File
@@ -1,44 +0,0 @@
//! Bootstrap binary — seeds initial system data idempotently.
//!
//! Creates default permissions, roles, and admin user if they don't
//! already exist. Run once after first deployment.
//!
//! Usage: cargo run --bin bootstrap
fn main() -> anyhow::Result<()> {
println!("Zesdex Bootstrap — seeding initial data...");
let store = zesdex_domain::core::Store::new();
store.ensure_dirs()?;
// Seed default settings if not present
let settings_path = store.base_dir.join("settings.json");
if !settings_path.exists() {
let settings = zesdex_domain::cms::Settings::default();
let content = serde_json::to_string_pretty(&settings)?;
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");
}
// Seed default app config if not present
let config_path = store.base_dir.join("app_config.json");
if !config_path.exists() {
let config = zesdex_domain::cms::AppConfig::default();
let content = serde_json::to_string_pretty(&config)?;
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");
}
println!("Bootstrap complete.");
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More