Files
zesdex/.claude/plans/zesdex.plan.md
T

1402 lines
101 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: zesdex — Rust TUI AI Coding + Security Agent (koma architectural clone)
**Source reference**: `/mnt/code/koma/` (Rust `src-agent` + Python `src-security` + Python `src-internet`)
**Complexity**: Large (multi-session, phased delivery)
## Summary
`zesdex` is a from-scratch Rust rebuild of koma's full architecture: a `ratatui` TUI
agent driven by an OpenRouter-compatible LLM backend, with a rich sandboxed tool set
(filesystem, shell, git, web, memory, sub-agents), a three-layer approval harness,
MCP client support, OAuth login, a detachable daemon/client IPC split for
multi-session use, and an opt-in security-tooling module wired to a Python sidecar
daemon (mirroring `koma_sec_daemon`) for **authorized** pentesting/CTF/research use
against systems the operator owns or is explicitly authorized to test.
Four explicit deviations from the literal 1:1 clone, all already confirmed with you:
1. **Approval harness stays, reshaped as an opt-in speed dial, not deleted.** Auto
mode is genuinely fast (WC + TAC run inline, no `y/n` prompts, mutations proceed
immediately) — that satisfies "unrestricted auto-approve at maximum speed." What
does **not** get removed: the deterministic **catastrophic-op guard**
(`check_destructive` in `git_operator`, the recursive-delete-outside-workspace
check, force-push-over-history, secret/credential-file exfil patterns in
`web_download`/`bash`). That guard is not a per-turn approval prompt — it never
blocks routine work — it is a hard-coded refusal on a short, named list of
irreversible operations, and it fires even in Yolo/Auto mode. Everything else
(file writes, edits, ordinary shell commands, git commits/branches, web fetches)
runs with zero prompts in Auto/Yolo mode, exactly as requested.
2. **Security toolkit ships as an explicit opt-in module** (`/security` panel,
disabled by default, requires the user to explicitly enable it per koma's own
`security_enabled`/`yolo_armed` gating pattern) with a startup banner and
`--security-install` flow that states the authorized-use scope, mirroring
koma's own "curated, opt-in suite... for authorized testing and research"
framing. `sec_*` tools remain harness-exempt (per koma's own design — these are
explicit user-invoked offensive actions, not silent mutations to gate), but the
catastrophic-op guard's network/destructive-target patterns still apply where
they'd protect the *operator's own machine* (e.g., a `sec_z3`/`sec_sage` script
that shells out to `rm -rf /`).
3. **koma's `task` subagent primitive (Phase 6) is replaced wholesale by a dynamic
workflow orchestration engine**, modeled directly on the `Workflow` tool this
very session runs on — not on anything in koma, which has no equivalent. See
"Dynamic Workflow Orchestration" in the digest below and the rewritten Phase 6.
Three decisions you confirmed:
- **Fan-out is model-decided, not forced.** Simple requests are handled inline by
the main agent with its own tools, exactly as today; the model only invokes the
workflow engine when it judges a task benefits from decomposition, parallelism,
or independent verification passes.
- **Full replacement, not a layer on top.** There is no separate simple
"delegate one subagent" tool anymore — a single-agent delegation is just a
one-line workflow script (`agent(...)` with no `parallel`/`pipeline` wrapper),
so the workflow engine is the *only* delegation surface, keeping one mental
model instead of two.
- **UI: a separate live progress-tree panel**, auto-shown the instant a workflow
starts (phase → agent → status), dismissible/re-summonable without killing the
run, while the Main Execution Log keeps behaving exactly as specified for
everything else.
4. **NEW — Reasoned mutations + self-learning quality loop**, added after your
latest request: every `write`/`edit` tool call must carry a required `reason`
argument (rejected deterministically, before touching the filesystem, if
missing/empty), so every mutation is auditable after the fact. Once an agentic
turn finishes and any files were touched, zesdex automatically — no model
decision involved — spawns a detached background subagent (reusing Phase 6's
subagent engine in koma's existing detached-nudge mode) that reviews the
touched files against their stated reasons and the surrounding code's own
conventions, and folds anything worth keeping back into per-project memory as
a new `lesson`-typed entry. Because the memory index is already injected into
every future turn's system prompt (koma's existing mechanism, unchanged), this
closes a real feedback loop: code quality compounds across turns instead of
the agent repeating the same mistake every session. See "Reasoned Mutations &
Self-Learning Quality Loop" in the digest below and the new Phase 7.
Following your explicit request for concrete self-learning feature
suggestions, four core additions and three advanced additions were confirmed
and are folded into the same digest section and Phase 7 (advanced ones note
their own phase where they land instead): **outcome-linked learning** (lessons
backed by real `build`/`test` results, not just LLM opinion), **lesson
lifecycle** (decay/staleness + contradiction detection, never silent auto-
delete), **human-in-the-loop calibration** (a keep/discard toast before a
lesson becomes permanent, auto-keep in Auto/Yolo but always visible/
reversible), and a **global cross-project lesson tier** (mirrors the Builtin→
Global→Session tiering Phase 6 already builds for agent definitions) as core
Phase 7 scope; **batch/offline pattern mining**, a **`/learning` dashboard**
(delivered in Phase 14 alongside the rest of the remaining UI surface), and
**escalation on repeated violations** as the advanced layer on top.
A follow-up round of suggestions added three more confirmed additions, all
still Phase 7/Phase 14 scope: **lesson graduation into a real lint check**
(a lesson that survives repeatedly at `confidence: verified` stops being
just injected text and becomes a deterministic pre-write check), a
**session-end retrospective with per-lesson provenance** (one holistic
summary per session, plus every lesson traceable back to the exact turn/
edit that produced it), and **user-authored lessons with consensus-gated
global promotion and token-budget visibility** (`/lesson` to author a
`confidence: human` entry directly, a style-guide import path, 2-of-2
independent-reviewer agreement required before any lesson reaches `global`
scope, and the reviewer loop's own token spend surfaced on the usage
dashboard). Per your explicit requirement, **every one of these — and
everything already in Phase 7 — is language-agnostic by design**: nothing
in the self-learning loop hard-codes Rust or any single ecosystem
(build/test detection, lint graduation, and lesson content all generalize
across whatever language a given project/workspace is written in — zesdex
itself being written in Rust is incidental to this feature, not a
constraint on it).
A third round added six more confirmed additions, still Phase 7/Phase 14
scope: **shadow-mode enforcement** (a lint-graduation candidate runs silent
for a trial window, logging would-have-fired counts before ever surfacing
to the model, so a blunt rule is caught before it annoys anyone), **cross-
subagent learning within one workflow run** (a finding from one `agent()`
thunk can reach sibling thunks still running in the *same* `workflow_run`
call, not just the next turn/session via memory), **adaptive review
frequency** (consecutive empty review passes throttle the reviewer down;
fresh violations un-throttle it back up — the token-spend counterpart to
escalation), **quality trend metrics on the usage dashboard** (build/test
pass-rate, escalation count, and per-lesson trigger count charted over
time, with lessons that fire often but never correlate with fewer
escalations flagged as `forget` candidates), **structured lesson bodies**
(a `before`/`after` snippet pulled straight from the triggering diff,
alongside the prose description, so a lesson is concretely actionable, not
just abstract), and **lesson-pack export/import** (an opt-in, portable
bundle of a project's lessons for moving to another machine or sharing
with a team, independent of a shared `~/.zesdex/`).
Per your request to focus on the plan itself before any code, Phase 7 —
which had grown to cover all seventeen self-learning additions across
three rounds of suggestions — is now **split into Phase 7 (MVP core:
`reason`, the ledger, the automatic reviewer) plus four independently
deferrable sub-phases 7B (outcome-linking/lifecycle/calibration/global
tier), 7C (escalation/adaptive frequency/batch mining), 7D (lint
graduation, shadow-mode gated), and 7E (retrospectives/provenance/
user-authored/consensus/export/trend metrics)**, restoring the
"independently buildable/testable" property every other phase in this
plan already has. Phase 7 alone is a complete, working answer to your
original request; 7B7E are refinements you can build immediately after
or defer indefinitely. See the rewritten Phase 7 family in Tasks below.
No comments in generated Rust source, per your original instruction — self-documenting
naming instead. No stubs; every module in a phase is fully implemented before that
phase is considered done. Cargo.toml matches your requested metadata exactly.
## Reference Architecture Digest (from `/mnt/code/koma/`)
Captured here so later phases can cite it without re-reading the whole reference tree.
**Workspace**: Cargo workspace with a single `agent` binary crate (`src-agent`),
edition 2021, plus two out-of-process Python sidecars (`src-security`,
`src-internet`) invoked via subprocess/daemon, not compiled in.
**Core dependency set** (from `src-agent/Cargo.toml`, informs zesdex's `Cargo.toml`):
`ratatui 0.30`, `tokio` (`rt-multi-thread, macros, sync, time, net, io-util, signal`),
`reqwest` (`json, stream, blocking, native-tls-vendored`), `serde`/`serde_json`/
`serde_yaml_ng`, `anyhow`, `uuid` (`v4, v5`), `dirs`, `futures-util`,
`pulldown-cmark`, `syntect` (`default-fancy`), `rusqlite` (`bundled`), `ignore`,
`regex`, `globset`, `infer`, `base64`, `sha2`, `libc`, `rmcp` (MCP client:
`client, transport-child-process, transport-streamable-http-client-reqwest, macros`),
`include_dir`, plus web-scraping-adjacent crates (`dom_smoothie`, `fast_html2md`,
`scraper`, `url`, `percent-encoding`) for the lightweight (non-Python) web-fetch path.
**MVC data flow**: `KeyEvent → controller/input::handle_key → Action →
runtime/actions::apply_action → state mutation → view::draw`. Slash commands:
`controller/command::parse → Command → runtime/commands::apply_slash`. The
controller layer never mutates state; `apply_action`/`apply_slash` own all
mutation and async spawns; the view is read-only.
**Async/streaming bridge**: one `tokio::runtime::Runtime` created in `main.rs`; the
main loop is synchronous (`try_recv`/poll, no `.await` in the hot loop). One
`mpsc::unbounded_channel` per request stored in `active_rx`; harness verdicts ride
a **separate** `harness_rx` channel so they never interleave with stream tokens.
`StreamEvent` variants: `Token, Reasoning, Usage, ToolCalls, Done, Error, Compacted,
HarnessVerdict`.
**Agentic loop**: model streams `tool_calls`; on `Done`, `advance_turn` commits the
assistant message, then either ends the turn or runs `process_tools` → gate/execute
each call → `finish_tool_round` → re-invoke the model. Loop continues until no more
tool calls or `MAX_AGENT_STEPS` (40) is hit. No forced plan gate — tools run on the
first call unless the model explicitly requests Plan mode via `plan_enter`.
**`Tool` trait**: `name() -> &'static str`, `description() -> &'static str`,
`parameters() -> Value` (JSON Schema), `run(&self, ctx: &ToolCtx, args: &Value) ->
Result<String>`. `ToolCtx` carries `workspace`, `workspaces: Vec<PathBuf>`,
`dir_cache`, plus (per the fuller subagent report) memory dir, MCP manager, security
manager, bash-saving flag.
**Deterministic tool inventory** (`tool_is_risky`: `write | delete | edit | bash |
git_operator | web_download`): `read, grep, glob, write, edit, delete, bash,
bash_output, bash_kill, cd, dir_list, dir_cache_update, pong, remember, forget,
recall, task, task_output, task_kill, todowrite, web_fetch, web_search,
web_download, git_cred, git_operator, git_worktree, plan_enter, plan_ready,
seqthink` — plus dynamic `mcp__*` and `sec_*` tools. `DEFERRED_TOOLS` (blocking I/O
tools) run on a plain `std::thread`, never inline on the event loop or inside the
tokio runtime directly.
`write`/`edit` (koma reference, `tool/fs/write.rs` + `tool/fs/edit.rs`): `write`
takes `path` + `content`, always creates parent dirs, always writes/overwrites,
reindexes the dir cache. `edit` takes `path` + `old` + `new` + optional
`replace_all`, requires the file to exist, refuses (returns a string result, not an
error) if `old` is missing or non-unique unless `replace_all=true`. Neither carries
any notion of "why" in koma today — zesdex's `reason` requirement (see below) is a
net-new addition on top of this exact shape, not a koma pattern.
**Three-layer harness** (`app/harness.rs`): WC (workspace containment check,
deterministic, gates the whole turn) → PC (advisory prompt classifier, fire-and-
forget, fail-open) → TAC (per-risky-call classifier, intent-aware, mode-dependent
fail-open/fail-closed policy). `CLASSIFY_TIMEOUT = 12s`. Modes: `Auto, Normal,
Plan, Yolo` (`AgentMode::cycle`), Yolo requires explicit arming
(`yolo_armed`) and bypasses TAC/`y/n` entirely — only deterministic guards remain.
**`shell_filter`**: post-processes `bash`/`git_operator` stdout — smart per-command
filters (cargo, git status/log/diff) tried first, static regex spec table
(npm/pip/docker/make/wget-curl) as fallback, full raw output always teed to disk
when trimmed/truncated/non-zero-exit.
**`git_operator`/`git_worktree`**: direct argv exec (no shell), SSH key injection
via `git_cred`, `GIT_TERMINAL_PROMPT=0`, and a **named destructive-op guard**
(`check_destructive`) requiring explicit `confirm_destructive=true` for force-push,
`reset --hard`, `clean -f/-d/-x`, `branch -D`, force checkout/switch/restore,
`stash drop/clear`, `tag -d`, `update-ref -d`, `filter-branch`, `gc --prune`.
Worktrees live in a shadow dir outside the repo.
**Subagents**: up to 5 concurrent (`MAX_SUBAGENTS`), agent definitions are
frontmatter+markdown files merged Builtin→Global→Session tier, isolated
`Conversation` seed (own system prompt, no shared history with parent), own
restricted tool allow-list (always minus `task`/`task_output`/`task_kill`, and
zero MCP tools), engine runs the same commit/execute/classify loop non-
interactively with fail-closed risky-tool gating (no human to ask), completion
folds back via blocking-park / chat-append / **detached-nudge** depending on
invocation mode. The detached-nudge mode matters most for zesdex: it lets a
subagent run fully off the critical path and only surface a synthetic-user-turn
nudge (or, per Phase 7, a collapsed system note) once the session goes idle,
without blocking whatever the user does next.
**Background bash**: `run_in_background: true` bash calls spawn a plain OS thread
(not tokio — child process needs to run outside the runtime), answer the model
immediately with a job id, poll via `bash_output`, kill via `bash_kill`
(SIGTERM to the recorded pid), completion is toast + buffered synthetic-user-turn
nudge once the session goes idle.
**IPC/daemon**: **daemon-per-session**, not daemon-per-machine — each session UUID
gets its own headless process at `~/.<app>/run/<session_id>.sock`. Transport: Unix
domain socket, 4-byte-BE-length-prefixed JSON frames (`FrameReader` reassembles
split/coalesced reads, 64 MiB hard cap enforced at the prefix). `bind()` success is
the sole liveness oracle (never a PID file). Per-client connection task splits
into independent async read/write halves bridged to the **synchronous** daemon
loop via `std::sync::mpsc` (daemon loop stays sync so it shares turn-servicing
code with a hypothetical standalone/no-daemon mode). `ClientRequest`/`DaemonFrame`
message sets cover attach/detach/list/status/submit/shell/key/paste/approve/plan-
decision/new/quit/switch-foreground. Multiple clients can attach one daemon
(controller + viewers, promotion on controller disconnect). Self-exit after a
grace window once all sessions are tombstoned and no client is enrolled.
**Snapshot/diff**: daemon builds a pure `StateSnapshot` from live `AppState`;
per-client `diff()` decides `needs_full` (structural change → resend full
snapshot; correctness-first, "when in doubt, ask for a full snapshot") vs a
cheap `Vec<StateDelta>` for pure-append/status/scroll changes. Client applies
frames into a **shadow `AppState`** and reuses the *same* `view::draw` renderer,
so daemon and thin-client rendering can never diverge.
**Session locking**: one `session.lock` file per session holding the owner PID;
liveness via `kill(pid, 0)` (portable, not `/proc` parsing); stale locks
opportunistically cleared. Lock always written with the **daemon's** PID, never
the thin client's.
**MCP**: `rmcp`-based client, two transports (stdio child process, streamable
HTTP), tools namespaced `mcp__<sanitized_server>__<tool>`, `Local` vs `Proxy`
(shared global MCP daemon) backend behind one facade so multiple session-daemons
don't each spawn duplicate heavyweight MCP servers, sync↔async bridge for
`Tool::run` compatibility, 20s connect / 60s call timeouts, non-blocking
background connect/reconnect with a generation counter to discard stale in-flight
connects.
**OAuth**: PKCE for the primary provider (loopback `TcpListener` on a fixed local
port for the redirect, hand-parsed HTTP, CSRF `state` check), device-authorization
polling for a secondary provider, unverified local JWT payload decode for display
metadata only, single-flight per-provider refresh with a stale/near-expiry lead
window and an `unrecoverable` latch on `invalid_grant`.
**TUI layout**: header (mode indicator) → transcript (scrollable, markdown-
rendered assistant turns, plain-text user/streaming turns) → optional pending-
steer panel → model-name row → input box (dynamic height, capped at half the
frame) → status bar. Markdown rendering emits pre-wrapped `Vec<Vec<Span>>` so
line count matches on-screen rows exactly (needed for scroll math); `syntect`
lazy-singleton highlighting in fenced code blocks with hard-splitting on
over-width lines; tables/lists/blockquotes word-wrap via a shared char-level
wrapper. Theme: a `Palette` struct of ~12 semantic colors + a 5-stop heatmap
ramp, computed fresh per frame from config, several named palettes plus an
accent-color resolver.
**Mode state machine**: far more than "5 modes" — onboarding, provider-OAuth
onboarding, credentials form, session picker/hub, chat, loading splash, settings,
agents editor, MCP manager, help, effort picker, usage dashboard, message rewind,
quit confirm, security panel, background-bash panel, todo panel. Each mode is a
struct variant on one `Mode` enum, exactly one active at a time, stored per-session.
**Security sidecar** (`koma_sec_daemon` equivalent): long-lived Python child
process, newline-delimited JSON frames over stdin/stdout (not a socket) —
handshake with a shared token, `call`/`health`/`install` ops, single-threaded
serialized dispatch. Tool roster spans web (http, sqlmap, nuclei, ffuf, dalfox,
zap, xss-confirm), crypto (z3, sage, rsa, factordb, lattice/fpylll, hashcat,
hashid, generic decode), reverse-engineering (js deobfuscate/unminify, source-map
recovery, wasm decompile), and pwn (static triage, ROPgadget, pwntools remote
session, exploit-template scaffold). Sandbox = wall-clock timeout + optional
RLIMIT_AS/RLIMIT_CPU + optional (opt-in, currently-unused-upstream) bubblewrap
network unshare — no container/chroot/seccomp. Tiered installer (pip / GitHub-
release binary / gem / manual-detect-only) with a health-probe surfaced in the UI.
**Internet sidecar** (`scrapion_agent` equivalent): **stateless**, one-shot
subprocess per call (`python -m ... --json "<url-or-query>"`), Playwright-Firefox-
headless search+scrape with stealth tweaks, HTML→Markdown conversion, JSON out on
stdout. Rust side spawns it on a plain OS thread (not tokio) with a `recv_timeout`
guard and falls back to a raw HTTP fetch on any failure.
**Dynamic Workflow Orchestration** (new — not present in koma; modeled on this
session's own `Workflow` tool): a script-driven orchestration layer that lets the
main agent decompose a task into many subagents running in parallel, pipeline, or
phased stages, rather than koma's one-subagent-at-a-time `task` primitive.
- **Trigger**: exposed to the model as one new tool, `workflow_run(script, args)`.
There is no separate "spawn one subagent" tool — a single delegated task is
simply a one-`agent()`-call script, so `workflow_run` is the sole delegation
surface. The system prompt instructs the model to reach for it when a task
benefits from decomposition/parallelism/independent verification, and to skip
it (use its own `read`/`grep`/`edit`/`bash` directly) for anything simple —
this is model judgement, not a hard trigger rule, matching your "dynamic, model
decides" choice.
- **Script primitives** (a small embedded scripting surface, not a general
scripting language — deliberately narrow so it stays auditable and
serializable into the persisted transcript):
- `agent(prompt, opts?) -> AgentResult` — spawn one subagent (own isolated
conversation seed, own restricted tool allow-list, same non-interactive
engine loop as koma's subagent engine: commit/execute/classify with
fail-closed risky-tool gating since there's no human to ask). `opts` covers
`label`, `phase`, `model` override, `effort` override.
- `parallel(thunks) -> Vec<AgentResult>` — run N agent thunks concurrently,
barrier: all must finish before returning. Bounded by a concurrency cap
(mirrors koma's `MAX_SUBAGENTS`-style ceiling, default 5, configurable).
- `pipeline(items, stage1, stage2, ...) -> Vec<Any>` — run each item through
every stage independently with **no barrier between stages** (item A can be
on stage 3 while item B is on stage 1); the default recommended shape for
multi-stage fan-out because wall-clock is the slowest single chain, not the
sum of slowest-per-stage.
- `phase(title)` — label subsequent `agent()` calls under a named phase for
the progress-tree UI; purely cosmetic/organizational, no execution effect.
- `log(message)` — narrator line surfaced in the progress panel above the tree.
- **Execution model**: `workflow_run` itself is a `DEFERRED_TOOL` (per the
existing `DEFERRED_TOOLS` pattern) whose body runs on the tokio runtime (it
needs real concurrency for `parallel`/`pipeline`, unlike the blocking-thread
tools); each `agent()` call inside it reuses the exact same subagent spawn/
engine/fold-back machinery Phase 6 already needed for koma parity — so
`workflow_run` is a thin orchestration layer *over* individual subagent
spawns, not a reimplementation of subagent execution. Concurrency, isolation
(own `Conversation` seed, own tool allow-list, zero recursion into
`workflow_run` itself to bound blast radius), and the fail-closed risky-tool
gate are all inherited unchanged from the subagent engine design.
- **Result delivery**: `workflow_run` blocks the calling turn (like a normal
tool call) and returns a structured summary (per-agent status + final return
value) as the tool result once every stage completes; the model then
synthesizes a response from that structured result, same as any other tool
round-trip — no separate detached/nudge mode for v1 (koma's detached-task
nudge pattern is not carried over here, since workflow runs are expected to be
the user-visible unit of work, not a background fire-and-forget job).
- **TUI surface**: a dedicated **Workflow Progress panel**, auto-shown the
instant `workflow_run` starts (overlaying/pushing above the transcript region,
matching koma's overlay pattern for `Bash`/`Todo`/`MessageRewind` modes — same
`layout_chunks` reuse, new overlay content), rendering a live tree: phase →
per-agent row → status glyph (queued/running/done/error) → elapsed time.
Dismissible with Esc without killing the run (run keeps streaming in the
background; re-summon via a key binding or `$`-style panel toggle, mirroring
koma's sub-agents panel UX). On completion, the panel's final state persists
in scrollback as a collapsed summary block in the Main Execution Log, and the
panel auto-closes after a short delay unless pinned open.
- **Persistence**: each `workflow_run` call and its final structured result is
logged to the session transcript like any tool call/result pair — no separate
workflow-history store for v1.
**Reasoned Mutations & Self-Learning Quality Loop** (new — not present in koma;
added for your "tiap edit atau write harus ada parameter alasan... bg agent untuk
verifikasi kualitas... self learning" request):
- **`reason` becomes a required argument on `write` and `edit`** (koma's
reference shapes above gain one field each): `{"path", "content", "reason"}`
for `write`, `{"path", "old", "new", "replace_all"?, "reason"}` for `edit`.
Enforcement is deterministic and happens *before* any filesystem call, exactly
like the existing `arg_str` required-field pattern already used for `path`/
`content`/`old`/`new` — a missing or blank `reason` returns
`error: missing required string argument 'reason'` and the file is never
touched. This is intentionally the same shape as `tool_is_risky`/WC: a name-
based, always-on, non-negotiable check, not something the classifier or agent
mode can waive. `delete` and `bash` are explicitly NOT changed — the user's
ask was scoped to edit/write, and widening it would duplicate what the
harness/catastrophic-op guard already cover for those.
- **Every accepted call appends one line to an append-only per-session ledger**,
`<session_dir>/edits.jsonl` (same directory tier as `session.lock`/
`settings.json`, same atomic-append discipline as `model::memory::atomic_write`
uses for whole-file writes — here it's an `O_APPEND` line write, since the
ledger is never rewritten in place). Each line: `{ts, tool, path, reason,
content_sha256, bytes_delta}`. The ledger is the audit trail the reviewer
(below) reads; it is never truncated or rotated automatically in v1.
- **Auto-triggered review, not model-invoked.** When `advance_turn` reaches a
turn boundary (no more `tool_calls`, same point `Done` already fires today)
and that turn's slice of `edits.jsonl` is non-empty, the event loop — not the
model, not a tool call — spawns exactly one subagent using Phase 6's existing
engine in koma's **detached-nudge** invocation mode, seeded with a new
built-in agent-definition persona, `quality-reviewer`. Its tool allow-list is
read-only plus memory: `read, grep, glob, recall, remember`. It never sees
`write`/`edit`/`bash`, so it cannot itself mutate anything — a review pass can
only ever produce a memory note or nothing.
- Input: the turn's ledger lines (path + reason + diff-relevant content) plus
a `recall` of the project's existing `lesson`-typed memories, so the
reviewer can check "have I already said this" before writing a new one —
this is what keeps `MEMORY.md` from growing an unbounded stream of
restated observations. If an existing lesson already covers the point, the
reviewer does nothing (no duplicate `remember` call).
- Output: zero or more `remember(..., type="lesson")` calls for anything
genuinely worth carrying forward (a real convention violated, a recurring
smell, a reason that didn't match what the diff actually did), plus exactly
one short verdict string returned as the subagent's final result.
- The verdict is rendered as a **collapsed system note** in the Main
Execution Log (a new `StreamEvent`-adjacent variant, e.g. `SystemNote`,
routed the same way `Compacted`/`HarnessVerdict` already ride their own
channel so it can never interleave with live stream tokens) — it is
explicitly NOT appended to the model-visible conversation history, so
review passes cost the reviewer's own tokens only and never grow the main
agent's context.
- Because `model::memory::load_memory_index` already injects the whole
`MEMORY.md` index (now including any `lesson` entries) into every future
turn's system prompt, this is the entire feedback loop — no new injection
plumbing needed, only the new `kind` value and the auto-trigger.
- **Bounded, not free-running**: at most one reviewer subagent runs at a time
(reviews queue rather than pile up — reuses the same bounded-concurrency
primitive Phase 6 builds for `parallel()`, capacity fixed at 1 for this
queue); the trigger is skipped entirely while `AgentMode::Plan` is active
(nothing mutates in Plan mode, so `edits.jsonl` can't have a non-empty slice)
and skipped for edits made *by* a reviewer or workflow subagent itself (no
recursive review-the-reviewer chains — ledger lines carry an `origin: main |
subagent | reviewer` tag precisely so this filter is a cheap equality check,
not a heuristic).
- **Explicitly reuses, does not reinvent**: Phase 6's subagent spawn/engine/
fold-back machinery (detached-nudge mode already exists there for other
reasons); `model::memory`'s `write_memory`/`read_memory`/`slugify` primitives,
widened by one `kind` value; the existing pattern of a dedicated side-channel
for out-of-band events (`harness_rx` today, a parallel `notes_rx` for
`SystemNote`).
**Outcome-linked learning** (language-agnostic by design — zesdex itself being
a Rust project is incidental; the mechanism below never assumes Rust): the
reviewer's input is upgraded from "reasons + diff only" to "reasons + diff +
real verification result." Detection of "how do I build/test this project"
is a small ordered probe, not a hard-coded `cargo` call: a `verify_command`
in `settings.json` always wins if the user sets one; otherwise zesdex probes
for well-known project markers already visible to `dir_cache` (`Cargo.toml`
`cargo build && cargo test`, `package.json` → its `scripts.test`/`scripts.
build` if present, `pyproject.toml`/`requirements.txt``pytest`/`python -m
pytest` if discoverable, `go.mod``go build ./... && go test ./...`, and so
on for the other common ecosystems); if no marker matches, verification is
simply skipped for that project and the reviewer runs on reasons+diff alone.
This probe table lives in one named function (mirroring the `tool_is_risky`/
catastrophic-op-guard pattern of "one place to extend"), so adding a new
ecosystem later is a one-line addition, not a redesign. The resolved command
runs on a plain `std::thread`, capped at a short timeout, and hands the
reviewer the pass/fail + trimmed output alongside the ledger. A lesson born
from an actual build/test failure is written with `confidence: verified`; a
lesson born from the reviewer's own judgement alone (no reproducible failure)
is written with `confidence: opinion`. Both render in the memory index, but
`opinion` entries are visually distinguished (dimmer/marked) in every mode
that lists memories, so verified lessons are never confused with the
reviewer's own guesses. If the verification command itself is unavailable or
misconfigured, the reviewer still runs on reasons+diff alone — this is a
strict enhancement, never a hard dependency.
**Lesson lifecycle**: every `lesson` entry gains two frontmatter fields beyond
koma's existing `name/description/type``confidence` (`verified | opinion`,
from outcome-linked learning above) and `last_confirmed` (a timestamp, bumped
whenever a later review cites the same lesson as still applicable). A
lightweight idle-time sweep (piggybacks on the same idle-detection the
background-bash completion nudge already uses, no new polling loop) flags —
never silently deletes — any `lesson` whose `last_confirmed` is older than a
configurable staleness window as `stale: true` in its frontmatter; a stale
lesson still gets injected into the index (so it's still visible) but is
labeled so the model and the user both know it may no longer hold. Separately,
before any new `remember(type="lesson")` call is accepted (both from the
regular reviewer and human-in-the-loop confirmation below), a cheap
recall-and-compare pass checks the new lesson's text against existing lessons
for direct contradiction (e.g. "always use X" vs. a new "never use X"); a
detected contradiction blocks the auto-save and instead surfaces both entries
side by side in the calibration toast for a human decision — contradictions
are never silently resolved by whichever write happens to land last.
**Human-in-the-loop calibration**: a `remember(type="lesson")` call from the
quality-reviewer does not go straight into `MEMORY.md`. It lands first in a
small pending queue (one more small JSON file alongside `edits.jsonl`,
`pending_lessons.json`, same atomic-write discipline) and renders as a
collapsed toast in the Main Execution Log: `Lesson baru: <description>
[keep]/[discard]`, using the same `SystemNote` side-channel Phase 7 already
adds. In Auto/Yolo mode the toast auto-resolves to `keep` after a short grace
window (matching the "unrestricted, no prompts" mandate for routine flow) but
the toast and its outcome remain visible/scrollable in the log and reversible
at any time via `forget`; in Normal/Plan mode (where a human is already
expected to be attentive) the toast waits for an explicit keypress instead of
auto-resolving. Either way, nothing about this blocks the *main* agent's own
turn — calibration only gates the reviewer's side-channel writes, never the
user-facing conversation.
**Global cross-project lesson tier**: mirrors the Builtin→Global→Session
tiering Phase 6 already builds for agent-definition files. A `lesson` memory
gains a `scope` field (`project | global`); `project` is the existing default
(`<pwd_bucket_dir>/memory/`, unchanged), `global` writes instead to a new
`~/.zesdex/memory/` store shared across every project. The reviewer defaults
every lesson to `project` scope; a lesson is only promoted to `global` when
either (a) the human-in-the-loop toast is used to explicitly promote it, or
(b) the escalation-on-repeated-violations mechanism (below) fires the same
lesson's underlying pattern across two or more distinct projects — never
silently promoted by the reviewer's own judgement alone, since a
project-specific convention wrongly promoted to global would pollute every
other project's context. `load_memory_index` (Phase 4/existing koma mechanism)
is extended to concatenate the global index ahead of the project index when
building the system prompt, so global lessons are always present and project
lessons can still override/refine them by appearing after.
**Batch/offline pattern mining** (advanced, Phase 7 scope but lower priority
within it): beyond the per-turn reviewer, an idle-triggered (same idle
detection as the lifecycle sweep) deeper pass periodically walks the full
`edits.jsonl` history (not just the latest turn's slice) plus `git log -p`
for the same file set, looking for cross-turn patterns a single-turn reviewer
structurally cannot see (e.g., "every time `parser.rs` changes, the matching
test file does not"). Runs as one more detached subagent, same allow-list and
confidence/calibration path as the per-turn reviewer — it is the same
mechanism at a longer time horizon, not a separate code path. Rate-limited to
at most once per idle window so it can never compound with the per-turn
reviewer into runaway subagent spawning.
**Escalation on repeated violations**: every accepted ledger line is checked
(cheap substring/embedding-free heuristic first — a lesson's `slug` matched
against the touched file's recent `edits.jsonl` history — deferred to the
reviewer's own judgement only when the heuristic is ambiguous) against the
existing lesson index for a *repeat* violation of something already
`remember`ed. On the third repeat of the same lesson within a configurable
window, zesdex stops treating it as routine background learning and instead
surfaces a non-collapsed, attention-grabbing note directly in the Main
Execution Log (not the quieter `SystemNote` styling used for ordinary
verdicts) — the premise being that if index-injection alone hasn't stopped
the pattern after three tries, silently trusting the system prompt again is
not working and a human should see it. This counter is also the trigger
condition that can promote a lesson from `project` to `global` scope per the
tiering feature above, when the same pattern repeats across multiple distinct
project directories rather than one.
**Lesson graduation into a lint check** (language-agnostic — the check itself
is expressed as a pattern-matching rule against the tool call's own arguments/
diff text, never a Rust-specific AST): once a lesson has stayed at
`confidence: verified` and been cited by the escalation counter above at
least once (i.e., it has already proven itself both true and still being
violated), it becomes eligible for graduation. A graduated lesson gains a
`check` frontmatter field — a small structured rule (substring/regex against
the file's new content, or a path-glob + required-companion-file rule, e.g.
"editing `parser.*` without also touching a matching `*test*` file") — and
from that point on is enforced the same way the `reason` argument is: a
deterministic, always-on check inside `write`/`edit`, evaluated *before* the
harness/classifier layer, same shared pattern as `arg_str`. A failed
graduated-lesson check does not silently block the write; it returns a
result string naming the lesson and asking the model to address it or
explicitly override with a `reason` that acknowledges the exception — so
graduation raises the bar without ever becoming an unbreakable wall. Only a
human (via `/learning`, Phase 14) or the consensus-promotion mechanism below
can graduate or de-graduate a lesson — the reviewer itself cannot
self-promote into an enforced check, for the same "no single opinion becomes
policy" reason `global` promotion is gated.
**Session-end retrospective + per-lesson provenance**: every `lesson` memory
gains a `provenance` field recording the session id + turn index + ledger
line hash it was born from (or, for a `/lesson`-authored entry below, a
literal `"user"` marker instead) — a cheap addition since all of that is
already available at the point `remember` is called, purely for later
auditability (`/learning` can jump from a lesson straight to the originating
edit). Separately, when a session ends (explicit `/quit` or the daemon's
tombstone-on-idle path, Phase 11), one more detached subagent — reusing the
identical `quality-reviewer`-style engine, not a new code path — is hand a
session-wide view (every ledger line + every lesson touched this session,
not just the last turn) and produces a single holistic summary memory
(`type: retrospective`, one per session, not one per turn) capturing things a
per-turn reviewer structurally cannot ("this session repeatedly restructured
the same module," "three different lessons this session all pointed at the
same underlying gap"). Retrospectives are additive and separate from
`lesson` entries — they are not injected into the system prompt by default
(they're a human-facing summary, browsable from `/learning`), so they cannot
bloat future turns' context the way an unbounded `lesson` count could.
**User-authored lessons, consensus-gated global promotion, token-budget
visibility**: a new `/lesson <text>` slash command lets the user author a
`lesson` memory directly, tagged `confidence: human` — a rank above both
`verified` and `opinion`, since it is authoritative by construction and
therefore exempt from the contradiction-block/calibration-toast path
entirely (a human overriding a prior automated lesson is always accepted
immediately, never queued). A companion `/lesson import <path>` command feeds
an existing convention document (`CONTRIBUTING.md`, a style guide, any
markdown file the user points at) through the same `quality-reviewer`-style
subagent to extract candidate lessons in bulk, seeding the project's memory
from day one instead of starting from zero — extracted entries still land at
`confidence: opinion` (the source document is trusted, but the *extraction*
is still an LLM judgement call) and go through the normal calibration toast
before becoming permanent. Global promotion, already gated behind either
explicit human calibration or the multi-project repeated-violation counter
(per the tiering feature above), is tightened further: an automatic
(non-`/lesson`) promotion additionally requires two independent reviewer
subagents (spawned fresh, no shared context between them — cheap since
review subagents are already isolated by design) to agree the pattern
generalizes, before it's written to `~/.zesdex/memory/`; a single reviewer's
opinion is never sufficient for a `global`-scope write. A `/lesson`-authored
entry can still be promoted to `global` directly by the user with no
consensus requirement, same "human is authoritative" exemption as above.
Finally, every reviewer/retrospective/batch-mining subagent's token usage is
tagged with a `origin: self-learning` cost-ledger category (reusing whatever
per-turn cost tracking the usage dashboard, Phase 14, already accumulates for
the main agent) and surfaced as its own line item there, so the self-learning
loop's token cost is always visible against the session total rather than
invisibly folded into "background work."
**Shadow-mode enforcement**: a lint-graduation candidate (per the graduation
mechanism above) does not go straight from `confidence: verified` to an
active `check`. It first spends a configurable trial window (N subsequent
`write`/`edit` calls against matching files, default a small fixed count) in
`shadow: true` state: the same rule evaluation runs on every matching call,
but a match only increments a would-have-fired counter on the lesson's own
frontmatter — it never returns anything to the model and never blocks
anything. Only once the trial window closes does the rule either graduate to
a real active `check` (per the existing enforcement path) or, if its
would-have-fired rate looks too indiscriminate (fires on most edits to a
matching file rather than the specific pattern it was meant to catch — a
simple ratio threshold, not a judgement call), get flagged back down to a
plain `opinion` lesson instead of ever reaching enforcement. This reuses the
exact same rule-evaluation code path enforcement already needs (Phase 7); the
only new state is the `shadow`/`trial_count` pair on the lesson and the one
branch deciding graduate-vs-demote when the window closes.
**Cross-subagent learning within one workflow run**: today, a finding one
`agent()` thunk makes only reaches the rest of the system on the *next* turn,
once the reviewer has run and the memory index has been rebuilt — too late
to help sibling thunks still executing in the *same* `workflow_run` call.
`workflow_run`'s existing execution context (Phase 6) gains one small
in-memory, run-scoped broadcast channel (not persisted, not `MEMORY.md`
this is intentionally ephemeral and scoped to the single run, distinct from
the durable per-project lesson store): any subagent spawned via `agent()` can
call a new restricted tool, `note_finding(text)`, whose only effect is to
push `text` onto that run's shared findings list; every *other* subagent
still active in the same run receives the current findings list prepended to
its next tool-round system context, cheaply (no re-spawn, no interruption of
in-flight work — it lands on the subagent's next turn boundary like any other
context update). Findings are ephemeral to the run: they do not automatically
become `lesson` memories (that still requires the normal reviewer pass after
the workflow completes, reading the same `edits.jsonl` ledger as any other
turn) — this feature closes the *within-run* propagation gap, not the
long-term memory gap, which the rest of Phase 7 already covers.
**Adaptive review frequency**: the per-turn reviewer trigger gains a small
run-length counter — consecutive review passes that produce zero new lessons
(no `remember` calls at all, verified or opinion) increment it; any pass that
produces at least one lesson resets it to zero. Past a small threshold of
consecutive empty passes, the trigger throttles: instead of reviewing every
qualifying turn, it skips a growing number of turns between reviews (simple
backoff, not a fixed schedule), directly reducing the `self-learning`-tagged
token spend the usage dashboard now surfaces. The moment a review does find
something (or the escalation counter fires, or a graduated `check` matches),
the throttle resets to zero immediately — the system is quick to re-engage
and only slow to relax, so a real emerging problem is never delayed by a
prior quiet streak. This is the direct token-cost counterpart to escalation
(escalation raises alarm on repeated *violations*; this lowers cost on
repeated *silence*), and both read the same underlying review-history data,
just in opposite directions.
**Quality trend metrics on the usage dashboard**: alongside the `self-
learning` token-cost line item already planned, the usage dashboard (Phase
14) accumulates and charts three time series purely from data Phase 7's other
features already produce (no new instrumentation): build/test pass-rate from
outcome-linked verification runs, escalation-note count from the
repeated-violations mechanism, and per-lesson trigger count from the same
counter escalation already reads. A lesson whose trigger count is high but
whose citations never precede a drop in the escalation rate (i.e., it keeps
firing without the underlying pattern ever actually going away) is flagged
as a `forget` candidate in `/learning` — a lesson that keeps getting matched
but never changes behavior is more likely to be miscalibrated or ignored
than genuinely useful, so it's surfaced for human review rather than kept
accumulating silently.
**Structured lesson bodies**: a `lesson` memory's body gains an optional
`before`/`after` code-snippet pair alongside its existing prose description —
extracted directly from the specific ledger line / diff that triggered the
lesson (the reviewer already has this content in hand when it calls
`remember`; this is a shape change to what it passes, not new data
collection). Rendering (in `/learning`, in the collapsed verdict note, and
implicitly in whatever text gets injected into future system prompts) shows
the snippet pair when present, falling back to prose-only for lessons that
predate this field or for which no single before/after pair meaningfully
captures the point (e.g. a cross-cutting retrospective observation). This
makes a lesson concretely actionable — "change shape X to shape Y" — rather
than purely descriptive, without requiring any new tool or trigger.
**Lesson-pack export/import**: an opt-in `/lesson export <path>` command
serializes a project's (or, with a flag, the global store's) `lesson`
entries — full frontmatter including `confidence`/`scope`/`provenance`/
`check` state, structured bodies, everything — into one portable bundle file,
independent of and without requiring a shared `~/.zesdex/`. `/lesson import
<path>` on another machine (or by a teammate) merges a pack into the local
store using the exact same recall-first dedup and contradiction-detection
path new reviewer-authored lessons already go through (Phase 7's lifecycle
mechanism, reused verbatim — an imported pack is just another source of
candidate lessons, not a privileged bypass), so imported entries land in the
calibration queue exactly like any other non-`/lesson` addition unless they
came from a `/lesson`-authored source originally (in which case they keep
their `confidence: human` standing on import too, since that's a property of
the entry, not of the transport). Export/import is explicitly scoped to
`lesson`/`retrospective` memory types only — it does not touch session
transcripts, `edits.jsonl`, or anything else in the per-session directory.
## Patterns to Mirror
| Category | Source | Pattern |
|---|---|---|
| State mutation | `app/runtime/actions/*.rs`, `app/runtime/commands/*.rs` | All state mutation flows through `apply_action`/`apply_slash`; controller/view layers are read-only translators |
| Tool definition | `tool/mod.rs` `Tool` trait | Zero-size struct per tool, `name/description/parameters/run`, registered in one `all_tools()` vec, risk flag is a name-based match in one place |
| Required-argument enforcement | `tool/fs/*.rs` `arg_str` helper | Missing/blank required args fail deterministically before any I/O, via one small shared helper — the pattern zesdex's new `reason` field reuses verbatim |
| Async bridge | `app/runtime/stream/*.rs` | One `mpsc` channel per request; main loop stays synchronous; blocking tool I/O deferred to `std::thread`, not `.await`ed inline |
| Approval gating | `app/harness.rs`, `stream/tools/approval.rs` | Deterministic check → advisory background classifier → per-call intent-aware classifier, each layer independently timeout-bounded and explicit about fail-open vs fail-closed |
| Memory persistence | `model/memory.rs` | Index-of-pointers (`MEMORY.md`) + one frontmatter+body file per entry, atomic rename-over-write, hard-sanitized slugs, only the index (never full bodies) injected into the system prompt |
| IPC framing | `ipc/frame.rs` | 4-byte-BE length prefix + JSON, hard cap enforced before payload allocation |
| Daemon liveness | `ipc/server.rs`, `model/session_lock.rs` | `bind()`/`connect()` success is the liveness oracle, never a PID file; lock files store PID + `kill(pid,0)` liveness check |
| Config layout | `model/settings.rs`, `model/app_config.rs` | Global prefs in one small `config.json`; all per-session config in `settings.json` under a per-session directory |
| Sidecar protocol | `koma_sec_daemon/protocol.py` | Newline-delimited JSON frames, single shared secret handshake, single-threaded serialized dispatch, every error surfaces as a string result, never a crash |
## Files to Create (Phase 1 scope; later phases add modules incrementally)
| File | Action | Why |
|---|---|---|
| `Cargo.toml` | CREATE | Package metadata + dependency set per your spec, extracted from koma's `src-agent/Cargo.toml` |
| `src/main.rs` | CREATE | Entry point: terminal setup/teardown, tokio runtime construction, mode dispatch into the event loop |
| `src/app/mod.rs`, `src/app/state.rs`, `src/app/mode.rs` | CREATE | `AppState`, `Mode` enum (Chat + Onboard to start), core runtime state |
| `src/app/runtime/mod.rs`, `event_loop.rs`, `actions.rs`, `commands.rs`, `stream.rs` | CREATE | Synchronous main loop, `Action`/`Command` dispatch, tokio stream bridge |
| `src/app/harness.rs` | CREATE | WC/PC/TAC three-layer approval harness + catastrophic-op guard |
| `src/controller/input.rs`, `src/controller/command.rs` | CREATE | Key→Action translation, slash-command parsing |
| `src/view/mod.rs`, `view/chat.rs`, `view/theme.rs`, `view/markdown.rs`, `view/status.rs` | CREATE | Ratatui rendering: 3-region chat layout + status bar per your spec |
| `src/tool/mod.rs`, `tool/fs.rs`, `tool/search.rs`, `tool/shell.rs`, `tool/shell_filter/*.rs` | CREATE | `Tool` trait + filesystem/search/shell tool set with output filtering; `write`/`edit` schemas include required `reason` from day one |
| `src/tool/git_operator.rs`, `tool/git_worktree.rs`, `tool/git_cred.rs` | CREATE | Git tool set with destructive-op guard |
| `src/dto/chat.rs`, `dto/openrouter.rs` | CREATE | Wire types for the LLM API (OpenRouter-compatible) |
| `src/service/mod.rs`, `service/openrouter.rs` | CREATE | Streaming client, `StreamEvent`, classifier calls |
| `src/model/session.rs`, `model/settings.rs`, `model/store.rs`, `model/app_config.rs`, `model/conversation.rs` | CREATE | Session persistence, per-session settings, config |
| `src/resources.rs`, `src-misc/*.txt` | CREATE | Embedded system/personality/tool-usage/classifier prompts |
Later phases (see Tasks) add: `src/model/memory.rs` + `src/model/editlog.rs`
(reasoned-edit ledger + `lesson`-typed memory, Phase 7), `src/app/review/mod.rs`
(auto quality-review trigger, Phase 7; extended in-place by 7B/7C/7D/7E — no
new top-level module per sub-phase), `src/app/subagent/*` +
`src/app/workflow/{mod,script,engine}.rs` + `src/tool/workflow.rs` +
`src/view/workflow.rs` + `src/app/mode` workflow-panel overlay (dynamic workflow
orchestration, replaces koma's `task`, Phase 6), `src/app/bgbash/*` (background
jobs), `src/app/mcp/*` (MCP client), `src/service/oauth/*` (OAuth), `src/ipc/*`
(daemon/client split), `src/tool/internet/*` (web fetch/search/download),
`src/app/sec/*` + a `security-sidecar/` Python package (security toolkit),
`src/model/msglog.rs` (SQLite blob archive), `src/app/runtime/shortsend.rs`
(token-efficiency rail).
## Milestones, Dependency Graph, and Implementation Gates
This section turns the long phase list below into an executable roadmap. It does
not remove scope; it groups the work so implementation can proceed in stable,
reviewable increments without letting the advanced self-learning surface block
the basic product from becoming usable.
### Milestone groups
| Milestone | Phases | Outcome | Can stop here? |
|---|---|---|---|
| **M0 — Plan freeze** | Current document | Scope, risks, dependencies, and acceptance are frozen before code starts | Yes — no code written yet |
| **M1 — Native TUI shell + agent loop** | 1 → 2 → 3 | A usable single-process TUI that can stream model output, run core tools, auto-approve routine work, and still refuse catastrophic operations | Yes — first functional developer preview |
| **M2 — Durable sessions + context economy** | 4 → 5 | Sessions survive restarts, `/resume` works, long conversations stay usable via SQLite archive + short-send shaping | Yes — usable daily-driver baseline without subagents |
| **M3 — Dynamic workflow orchestration** | 6 | `workflow_run` replaces koma's `task`, supports many subagents, progress panel, `parallel`/`pipeline`, and run-scoped `note_finding` | Yes — first multi-agent release |
| **M4 — Self-learning core and growth path** | 7 → 7B → 7C → 7D → 7E | Required edit reasons, audit ledger, background reviewer, lesson lifecycle, escalation, enforced checks, retrospectives, import/export, dashboard data | Yes after any sub-phase; 7 alone is useful |
| **M5 — Background jobs + extensibility + auth** | 8 → 9 → 10 | Long-running shell jobs, MCP tools, and OAuth login are integrated without blocking the TUI | Yes — plugin/provider-ready release |
| **M6 — Detachable multi-session runtime** | 11 | Daemon/client split, attach/detach, snapshot/diff/shadow-state rendering | Yes — multi-session release |
| **M7 — External capability packs** | 12 → 13 | Authorized security toolkit and full internet/search/fetch/download stack land behind their respective opt-in controls | Yes — full agent capability release |
| **M8 — Full UI parity polish** | 14 | Settings, agents editor, MCP browser, usage dashboard, `/learning`, help, rewind, quit confirm, session hub | Yes — full koma-parity UI release |
### Dependency graph
```text
M0 plan freeze
Phase 1 TUI skeleton
Phase 2 streaming agent loop + core tools
Phase 3 approval harness + catastrophic-op guard
Phase 4 session persistence
Phase 5 SQLite archive + short-send
Phase 6 workflow orchestration
├─ requires Phase 2 tool loop
├─ requires Phase 3 non-interactive risky-tool gating for subagents
└─ provides subagent engine used by Phase 7+
Phase 7 self-learning MVP
├─ requires Phase 6 detached subagent execution
├─ requires Phase 4 session directory for edits.jsonl
└─ provides lesson memory + review side-channel
Phase 7B outcome/lifecycle/calibration/global tier
└─ requires Phase 7 lesson memory + review trigger
Phase 7C escalation/adaptive/batch mining
└─ requires Phase 7B confidence/scope/lifecycle fields
Phase 7D graduated checks + shadow mode
├─ requires Phase 7B lesson frontmatter fields
└─ benefits from Phase 7C repeated-violation counter
Phase 7E retrospectives/provenance/user lessons/export/trends
├─ requires Phase 7B confidence/scope model
├─ requires Phase 7C counters for trends
├─ requires Phase 7D checks for graduation UI
└─ rendered fully in Phase 14 dashboard
Independent after M3/M4 baseline:
Phase 8 background bash jobs
Phase 9 MCP client
Phase 10 OAuth login
Late structural split:
Phase 11 daemon/client IPC
├─ requires stable Phase 1 renderer
├─ requires stable Phase 4 session model
└─ should happen after Phase 8 so background nudges are daemon-aware
External packs:
Phase 12 security toolkit
├─ requires Phase 3 catastrophic guard
├─ requires Phase 4 settings/session storage
└─ benefits from Phase 11 daemon lifecycle, but can be built single-process first
Phase 13 internet module
├─ requires Phase 2 tool loop
├─ requires Phase 3 web_download guard adjacency
└─ benefits from Phase 8 background job patterns for sidecar timeout handling
Final UI parity:
Phase 14 remaining UI modes
├─ depends on the modes/features it surfaces
└─ can be partially implemented earlier for `/learning` if Phase 7E needs it
```
### Implementation gates
Each gate must pass before the next milestone starts. This is stricter than
"cargo build passes"; it prevents hidden partials from accumulating.
| Gate | Required before | Criteria |
|---|---|---|
| **G1 — TUI correctness gate** | Start Phase 2 | Phase 1 renders the exact 3-region layout, handles resize, input, scroll basics, and restores terminal state after panic/quit |
| **G2 — Tool-loop correctness gate** | Start Phase 3 | Phase 2 can complete a full mocked tool-use loop, including tool-call parse, tool execution, result append, and second model call |
| **G3 — Safety/speed gate** | Start Phase 4 | Auto/Yolo routine mutations have zero prompts; catastrophic-op guard blocks every named rule even in Yolo; no classifier failure can bypass deterministic guards |
| **G4 — Persistence gate** | Start Phase 5/6 | Sessions create/list/resume cleanly, stale locks are cleared, and no session file write can corrupt partial state on crash |
| **G5 — Context gate** | Start Phase 6 | Long conversation fixture proves SQLite archive and short-send shaping reduce wire payload without destroying transcript fidelity |
| **G6 — Workflow gate** | Start Phase 7 | `workflow_run` is the only delegation surface, `parallel` cap and `pipeline` no-barrier semantics are tested, progress panel survives dismiss/resummon, and `note_finding` is ephemeral/run-scoped |
| **G7 — Self-learning MVP gate** | Start 7B | `write`/`edit` require `reason`, ledger is append-only and exact, one detached reviewer runs per edited turn, and verdicts never enter model-visible history |
| **G8 — Lesson lifecycle gate** | Start 7C | Confidence, stale, scope, calibration, contradiction blocking, and global/project memory injection all have direct tests |
| **G9 — Escalation/adaptive gate** | Start 7D | Repeated-violation escalation and adaptive review throttling both work under synthetic histories and never run reviewers concurrently |
| **G10 — Graduated-check gate** | Start 7E | Shadow-mode trials, graduated checks, override behavior, and de-graduation are tested without making any check an unbreakable blocker |
| **G11 — Learning UI data gate** | Start Phase 14 `/learning` UI | Provenance, retrospectives, import/export, trend metrics, and self-learning token-cost records are all present as model/data APIs before UI work begins |
| **G12 — Daemon split gate** | Start Phase 11 | Single-process runtime is stable through Phases 110; renderer state can be snapshotted without borrowing live TUI objects |
| **G13 — External capability gate** | Start Phase 12/13 | Tool gating, settings, background subprocess supervision, and side-channel notes are stable enough that security/internet sidecars cannot destabilize core chat |
| **G14 — Full parity gate** | Finish Phase 14 | Every mode has an Esc/back path, every dashboard reflects live data, and every feature-specific acceptance item below remains green |
### Recommended implementation order
For the actual coding pass, the safest order is:
1. **M1**: Phases 13, because they produce the first runnable agent shell and the
always-on catastrophic guard.
2. **M2**: Phase 4, then Phase 5 only if long-session support is needed before
subagents; otherwise Phase 5 can be done immediately after Phase 6.
3. **M3**: Phase 6, because every self-learning feature depends on subagent
execution and workflow orchestration.
4. **M4**: Phase 7 only, stop, test it hard, then decide whether to continue with
7B7E immediately or ship the MVP learning loop first.
5. **M5/M6/M7/M8**: Phases 814 in order, except `/learning` UI slices from Phase
14 may be pulled forward after 7E if needed for manual calibration.
The important policy: **do not start Phase 7B before Phase 7 is stable, and do not
start Phase 11 before the single-process app is already boringly reliable**.
## Tasks
### Phase 1 — Skeleton: Cargo.toml, event loop, 3-panel TUI, status bar
- **Action**: Stand up the buildable skeleton exactly matching your explicit
layout spec: Main Execution Log (transcript), Input Prompt (bottom fixed-height
block), Status Bar (`[zesdex] | STATUS: AUTO-APPROVE (UNRESTRICTED) | PROTOCOL:
ZERO-STUBS ACTIVE`). Wire the synchronous main loop + tokio runtime bridge with
no LLM calls yet (a `pong`-style echo tool only), so the rendering/input/event
loop is provably correct before the agentic loop is layered on.
- **Mirror**: `app/runtime/event_loop/mod.rs` tick structure (drain stream events
→ drain harness verdicts → input poll with adaptive 8ms/100ms timeout → draw if
dirty); `view/chat/mod.rs` layout_chunks region split.
- **Validate**: `cargo build`, `cargo clippy -- -D warnings`, manual TUI smoke run
(type a line, see it echo into the log region, resize terminal, quit cleanly
restoring the terminal state).
### Phase 2 — OpenRouter-compatible client + streaming agentic loop
- **Action**: Implement `dto::chat`/`dto::openrouter` wire types, the streaming
SSE client, `StreamEvent` bridge, and `advance_turn`/`process_tools` loop
against a real filesystem/shell tool set (`read, grep, glob, write, edit,
delete, bash, dir_list, cd`). `write`/`edit` already require `reason` per the
Files-to-Create note above — enforced now, even though the review loop that
reads the ledger doesn't land until Phase 7, so the ledger has real data by
the time Phase 7 needs it. No approval gating yet — this phase proves the
model can drive tools end-to-end.
- **Mirror**: `service/openrouter.rs` streaming/classify pattern; `tool/mod.rs`
`Tool` trait and `all_tools()` registration; `MAX_AGENT_STEPS` loop cap.
- **Validate**: Integration test against a mock SSE server; a `write` call
without `reason` is rejected before any file is created; manual run against a
real OpenRouter-compatible endpoint editing a scratch file end-to-end.
### Phase 3 — Approval harness: Auto/Normal/Plan/Yolo + catastrophic-op guard
- **Action**: Implement WC (deterministic workspace containment), TAC (per-risky-
call classifier, intent-aware, mode-dependent fail-open/fail-closed), and the
`AgentMode` cycle (Auto/Normal/Plan/Yolo). Implement the **catastrophic-op
guard** as a small, explicit, always-on function independent of mode/classifier
state — modeled directly on koma's `git_operator::check_destructive` list
(force-push, `reset --hard`, `clean -f/-d/-x`, force checkout, recursive delete
outside any configured workspace root, credential-file read/exfil patterns) —
and extend it to `bash`/`delete`/`web_download` per your "keep a hard stop on
catastrophic ops" decision. Skip PC (advisory prompt classifier) as lower value
for v1; note it in follow-up.
- **Mirror**: `app/harness.rs` WC/TAC design, `git_operator.rs::check_destructive`.
- **Validate**: Unit tests per guard rule (force-push blocked without confirm,
`reset --hard` blocked, ordinary `git commit`/`write`/`edit` in Auto mode runs
with zero prompts and zero added latency vs. Phase 2 baseline).
### Phase 4 — Session model + on-disk persistence + `/resume`
- **Action**: `Session`/`Settings`/`AppConfig`/`store` (create/list/rename
sessions), per-session directory layout (`settings.json`, `messages.json`,
`session.lock`, `memory/`, `edits.jsonl`), session picker mode.
- **Mirror**: `model/session.rs`, `model/store.rs`, `model/session_lock.rs`
(PID-liveness lock, never a stale-file oracle).
- **Validate**: Kill `-9` a session mid-write, confirm lock is detected stale and
cleared on next list; `/resume` round-trips a full conversation.
### Phase 5 — SQLite blob archive + short-send token efficiency (optional-early)
- **Action**: `messages.sqlite` archive (append-only messages/blobs/summary
tables), non-destructive wire-payload compaction rail (`shortsend::shape`) so
budget models stay in-context on long sessions.
- **Mirror**: `model/msglog.rs`, `app/runtime/shortsend.rs` engage-hysteresis math.
- **Validate**: Long synthetic conversation exceeding context window compacts the
wire payload while `messages.json`/transcript stay uncompressed.
### Phase 6 — Dynamic workflow orchestration engine (replaces koma's `task`)
- **Action**: Build the subagent execution core first (isolated `Conversation`
seed, restricted tool allow-list, non-interactive engine loop with fail-closed
risky-tool gating — this part *is* still a direct mirror of koma's subagent
engine design, just not exposed as a standalone `task` tool). Layer the
`workflow_run(script, args)` tool on top with the four primitives
(`agent`/`parallel`/`pipeline`/`phase`/`log`), a bounded concurrency pool
(default 5, configurable), and the Workflow Progress panel (new `Mode`
overlay reusing `layout_chunks`, live phase→agent→status tree, dismiss/
re-summon without killing the run). Update the system prompt (`src-misc/
system-tools.txt` equivalent) to describe `workflow_run` and give the model
concrete guidance on when decomposition is worth it vs. handling inline.
Agent-definition format (frontmatter+markdown, tiered Builtin→Global→Session
merge) is still implemented so `agent()` calls can reference named personas,
and so Phase 7's built-in `quality-reviewer` persona has somewhere to live.
- **Mirror**: `app/subagent/{context,engine,event,spawn}.rs`, `model/
agent_def/*.rs` for the execution core; `view/chat/subagents.rs` +
`app/mode/mod.rs` overlay-mode pattern for the progress panel's UI shell (no
koma source for the orchestration script layer itself — that's new, specified
above under "Dynamic Workflow Orchestration").
Also add `note_finding(text)` — a restricted tool available only inside a
`workflow_run` execution context (not part of a standalone subagent's
allow-list) that pushes `text` onto a run-scoped, in-memory, non-persisted
findings broadcast; every other `agent()` thunk still active in the same run
receives the current findings list at its next tool-round boundary. This is
workflow-execution coordination, not lesson persistence, which is why it
lives here rather than in Phase 7 — a finding only becomes a durable
`lesson` if the normal Phase 7 reviewer independently decides to `remember`
it after the run, reading the same `edits.jsonl` ledger as any other turn.
- **Mirror**: `app/subagent/{context,engine,event,spawn}.rs`, `model/
agent_def/*.rs` for the execution core; `view/chat/subagents.rs` +
`app/mode/mod.rs` overlay-mode pattern for the progress panel's UI shell (no
koma source for the orchestration script layer itself, nor for
`note_finding` — both are new, specified above under "Dynamic Workflow
Orchestration").
- **Validate**: A `parallel()` workflow with 6 agent thunks confirms the 6th
queues on the concurrency cap; a `pipeline()` workflow with 3 items × 2 stages
confirms item A reaches stage 2 while item B is still on stage 1 (no barrier);
progress panel updates live during a real multi-agent run and correctly
survives Esc-dismiss-and-resummon without losing state; a trivial single-step
request in a manual smoke test is handled inline by the model without
invoking `workflow_run` at all (confirms the "dynamic, not forced" trigger
behavior is actually working, not just documented); inside a `workflow_run`
with two concurrent `agent()` thunks, a `note_finding` call from one is
observably visible to the other's next tool round, and is confirmed absent
from `MEMORY.md` after the run (ephemeral, not persisted).
### Phase 7 — Reasoned mutations + self-learning quality loop (MVP core)
Phase 7 was originally one block covering every self-learning idea explored
in planning, but that made it far larger and more tightly coupled than every
other phase in this plan — a violation of the plan's own "each phase is
independently buildable/testable" principle. It is now split into this core
phase plus four additive sub-phases (**7B****7E**, deliberately lettered
rather than renumbered so Phases 814 below are untouched). **Phase 7 alone
is the complete, working answer to your original request** — required
`reason`, an audit ledger, and an automatic background quality pass; 7B7E
are refinements layered on top, each independently deferrable if you want a
smaller first slice of the self-learning feature specifically. Anywhere the
Risks/Acceptance tables below say "Phase 7," read it as "the 7/7B/7C/7D/7E
family" unless a sub-phase is called out explicitly.
- **Action**: Add the required `reason` argument to `write`/`edit` (schema +
deterministic rejection before any filesystem call, before any I/O); implement
the append-only `edits.jsonl` ledger with an `origin: main | subagent |
reviewer` tag on every line; add a `lesson` memory kind to `model::memory`
(`name/description/type` only for now — the richer frontmatter fields land
in 7B7E) plus a recall-before-remember dedup convention so a duplicate
observation is never saved twice; add the built-in `quality-reviewer`
agent-definition persona (read-only + `recall`/`remember` allow-list, no
`write`/`edit`/`bash`, so a review pass can only ever produce a memory note
or nothing); wire the auto-trigger into the event loop's existing
turn-completion point (the same point `Done` already fires) so a non-empty
ledger slice for the just-finished turn spawns exactly one detached-nudge
subagent (Phase 6's engine, detached-nudge mode), capacity-1 queued if
another review is already running, skipped entirely in Plan mode and for
ledger lines whose `origin` is `subagent`/`reviewer` (no recursive
review-the-reviewer chains); add the `SystemNote` side-channel (parallel to
`harness_rx`) so the reviewer's verdict renders as a collapsed note in the
Main Execution Log without ever entering the model-visible transcript.
This is the complete MVP: reasoned mutations, an audit trail, and an
automatic background quality pass that folds observations into memory the
existing `load_memory_index` mechanism already injects every future turn.
- **Mirror**: `tool/fs/write.rs`/`edit.rs` (extend schema, reuse `arg_str`),
`model/memory.rs` (add `kind`, reuse `write_memory`/`read_memory`/
`slugify`/`atomic_write`), `app/subagent/spawn.rs` detached-nudge fold-back
(built in Phase 6), `app/runtime/event_loop/mod.rs` turn-completion hook,
`harness_rx`'s dedicated-channel pattern for the new `SystemNote` channel.
- **Validate**: A `write`/`edit` call missing `reason` is rejected before any
file is created/modified (unit test, both tools); a turn with 3 edits
produces exactly 3 ledger lines with matching reasons and `origin: main`; a
workflow subagent's own edits are tagged `origin: subagent` and do NOT
double-trigger a nested review; after a turn with edits, exactly one
reviewer subagent runs detached and the Main Execution Log gets exactly one
collapsed verdict note with zero additional turns appended to the
model-visible conversation; a deliberately duplicate lesson (same point,
reworded) is not re-saved because the reviewer's `recall`-first check finds
the existing entry; Plan mode never triggers a reviewer even after a
hypothetical mutation (defence in depth).
### Phase 7B — Outcome-linked learning, lesson lifecycle, calibration, global tier
Builds directly on Phase 7's `lesson` kind and reviewer trigger; independently
deferrable from 7C/7D/7E. See "Outcome-linked learning," "Lesson lifecycle,"
"Human-in-the-loop calibration," and "Global cross-project lesson tier" in the
digest above for full mechanism detail — summarized here as build items.
- **Action**: Extend `lesson` frontmatter with `confidence: verified |
opinion`, `last_confirmed`, `stale: bool`, and `scope: project | global`;
implement the language-agnostic build/test probe table (settings override →
ordered project-marker detection → skip if none match) as one named,
independently-extensible function, run on a plain thread and handed to the
reviewer alongside the ledger; implement the `pending_lessons.json`
calibration queue (auto-resolve-to-keep after a grace window in Auto/Yolo,
explicit keypress in Normal/Plan) with contradiction detection blocking
auto-save; implement the idle-triggered staleness sweep that flags (never
deletes) old `lesson` entries; implement the `~/.zesdex/memory/` global
store and extend `load_memory_index` to concatenate global-then-project.
- **Mirror**: `shell_filter`'s per-command-special-case pattern generalized
into the build/test probe table; `model/memory.rs` frontmatter extension;
`app/bgbash/*`'s idle-detection reused for the staleness sweep; `app/mode/*`
Builtin→Global→Session tiering pattern reused for the `project`/`global`
lesson split.
- **Validate**: A lesson born from a reproduced build/test failure is tagged
`confidence: verified` and one born from reviewer judgement alone is tagged
`opinion`, visibly distinguished; a deliberately contradictory lesson is
blocked from auto-save and surfaces both entries in the calibration toast;
in Auto mode a pending lesson auto-resolves to `keep` after the grace window
and remains `forget`-able afterward, in Normal mode it waits for an explicit
keypress; an artificially back-dated `last_confirmed` gets flagged
`stale: true` without deletion; a `scope: project` lesson does not leak into
a second project's injected index, while `scope: global` appears in both;
the build/test probe resolves correctly for at least three distinct
project-ecosystem fixtures with no ecosystem hard-coded outside the one
probe table.
### Phase 7C — Escalation, adaptive review frequency, batch pattern mining
Builds on 7B's lesson/confidence fields; independently deferrable. See
"Escalation on repeated violations," "Adaptive review frequency," and
"Batch/offline pattern mining" in the digest above.
- **Action**: Implement the repeated-violation counter that escalates to a
non-collapsed attention note (distinct styling from the routine collapsed
`SystemNote`) on the third repeat of a lesson within a configurable window,
and can promote a lesson from `project` to `global` scope when the repeat
spans multiple project directories; implement the consecutive-empty-pass
counter that throttles the per-turn review trigger's frequency and resets
to zero on any new lesson/escalation; implement the idle-triggered batch
pattern-mining pass over full `edits.jsonl` + `git log -p` history,
rate-limited to once per idle window and sharing the same capacity-1 review
queue as the per-turn reviewer.
- **Mirror**: `app/bgbash/*`'s idle trigger as the template for both the
batch-mining pass and the throttle counter's reset condition; ledger
`origin` tagging (Phase 7) reused as the cheap equality check for
repeat-detection scope.
- **Validate**: The same lesson pattern violated a third time within the
window produces a non-collapsed escalation note distinct from routine
verdicts; a synthetic run of N consecutive empty review passes measurably
reduces review frequency, and a single subsequent lesson-producing pass
resets it to reviewing every turn; batch mining never runs concurrently
with a per-turn review (shared capacity-1 queue verified under load).
### Phase 7D — Lesson graduation into enforced checks (shadow-mode gated)
Builds on 7B/7C; independently deferrable — a project can run indefinitely on
`lesson`-as-injected-text alone without ever enabling this sub-phase. See
"Lesson graduation into a lint check" and "Shadow-mode enforcement" in the
digest above.
- **Action**: Add the `check` frontmatter field (a small structured rule:
substring/regex against new content, or a path-glob + required-companion-
file rule) and its enforcement inside `write`/`edit` (same deterministic-
before-harness slot as `reason`) for graduated lessons — a failed check
never blocks the write, it returns a naming, addressable/overridable
result; gate graduation behind a mandatory shadow-mode trial window
(`shadow: bool` + `trial_count` frontmatter, same rule-evaluation path,
would-have-fired counting only, nothing surfaced to the model) with a
graduate-vs-demote decision at window close based on fire ratio; restrict
manual graduation/de-graduation to `/learning` (Phase 14) or the two-
reviewer consensus mechanism (Phase 7E) — never reviewer self-promotion.
- **Mirror**: `tool/fs/write.rs`/`edit.rs`'s `reason`-enforcement slot,
reused verbatim for `check` enforcement.
- **Validate**: A `check` candidate spends its full trial window in
`shadow: true` before ever surfacing anything to the model, and correctly
graduates or demotes at window close based on its fire ratio; a graduated
lesson's `check` fires on a violating `write`/`edit` as a named,
addressable result — never a silent or unbreakable block.
### Phase 7E — Retrospectives, provenance, user-authored lessons, consensus promotion, export/import, trend metrics
Builds on 7B (confidence/scope) and 7D (consensus gates graduation
promotion); independently deferrable — the MVP loop (Phase 7) and its
lifecycle/escalation refinements (7B/7C) are fully useful without this. See
"Session-end retrospective + per-lesson provenance," "User-authored lessons,
consensus-gated global promotion, token-budget visibility," "Quality trend
metrics on the usage dashboard," "Structured lesson bodies," and
"Lesson-pack export/import" in the digest above.
- **Action**: Add a `provenance` field to every `lesson` (session id + turn
index + ledger line hash, or `"user"` for `/lesson`-authored entries) and
the session-end retrospective trigger (reuses the daemon idle/tombstone
path, Phase 11, and the same reviewer-engine shape at session scope)
writing one `type: retrospective` memory per session, never injected into
the system prompt; add `/lesson <text>` and `/lesson import <path>` slash
commands (`confidence: human` for direct authorship, exempt from
calibration/contradiction; `opinion` for bulk-extracted imports, which DO
go through calibration); tighten automatic (non-`/lesson`) `global`
promotion to require two independently-spawned reviewer subagents to agree
before the write; tag every self-learning subagent's token spend with
`origin: self-learning` and surface it as its own usage-dashboard line item
(Phase 14), plus the three trend time series (build/test pass-rate,
escalation count, per-lesson trigger count) and the trigger-without-
improvement `forget`-candidate flag, all derived from data 7A7D already
produce; extend a `lesson`'s body with an optional structured `before`/
`after` snippet pulled from the triggering ledger line/diff; add `/lesson
export <path>` and `/lesson import <path>` (project or `--global` scope),
the latter routed through the exact same recall-first dedup/contradiction/
calibration path as any other new lesson, restricted to `lesson`/
`retrospective` memory types only.
- **Mirror**: `app/subagent/spawn.rs` detached-nudge fold-back (Phase 6,
reused unchanged for retrospective and two-reviewer consensus spawns);
`controller/command.rs` slash-command parsing for `/lesson`; koma's
existing usage-dashboard view, extended for the new cost category and
trend charts.
- **Validate**: Exactly one `type: retrospective` memory is produced per
ended session and is absent from the next session's injected system-prompt
index; `/lesson "text"` creates a `confidence: human` entry immediately
with no calibration toast; `/lesson import` seeds `opinion`-confidence
entries that DO go through calibration; automatic global promotion is
blocked when only one of two spawned reviewers agrees, and proceeds when
both do; the usage dashboard shows a nonzero `self-learning`-tagged token
total and correctly plotted trend charts against a synthetic session
history, with a high-trigger/no-improvement lesson flagged as a `forget`
candidate; a lesson with a structured `before`/`after` pair renders it in
both `/learning` and its injected system-prompt text; `/lesson export` then
`/lesson import` on a second (empty) project reproduces the same lesson set
with matching `confidence`/`scope`/`provenance`, and a near-duplicate
import is caught by the existing dedup path.
### Phase 8 — Background bash jobs
- **Action**: `run_in_background` bash execution on a plain OS thread, job
registry, `bash_output`/`bash_kill`, idle-triggered completion nudge.
- **Mirror**: `app/bgbash/mod.rs` thread/reader-thread/status-enum design.
- **Validate**: Long-running background command (`sleep 30 && echo done`) polls
correctly mid-run and nudges the model on completion without blocking the UI.
### Phase 9 — MCP client
- **Action**: `rmcp`-based client, stdio + streamable-HTTP transports, tool
namespacing/dedup, `/mcp` settings UI, config persisted in global config.
- **Mirror**: `app/mcp/mod.rs` connect/dispatch/sync-async-bridge design.
- **Validate**: Connect to a local test MCP stdio server, confirm its tools appear
namespaced in the model's advertised tool list and dispatch correctly.
### Phase 10 — OAuth login
- **Action**: PKCE authorization-code flow with a loopback callback listener,
token cache with single-flight refresh, `/settings` OAuth submenu.
- **Mirror**: `service/oauth/{pkce,loopback,manager}.rs` design.
- **Validate**: Full browser round-trip against a real OAuth-compatible provider
in a sandboxed test app registration.
### Phase 11 — Daemon/client IPC split (detachable multi-session)
- **Action**: Unix-socket, length-prefixed JSON-frame IPC; daemon-per-session
process model; snapshot/diff/shadow-state client rendering reusing the Phase 1
view layer unmodified; CLI daemon subcommands (`status/kill/restart/clean`).
- **Mirror**: `ipc/{frame,server,client,conn}.rs`, `ipc/snapshot/*.rs`,
`app/runtime/client/*.rs`, `app/runtime/event_loop/daemon/*.rs`.
- **Validate**: Detach a running session, confirm the daemon keeps streaming in
the background; reattach and confirm the shadow state matches exactly (no
full-vs-delta divergence) after a burst of token/tool-call activity.
### Phase 12 — Security toolkit (opt-in, authorized-use only)
- **Action**: `security-sidecar/` Python package mirroring `koma_sec_daemon`
(newline-JSON stdin/stdout protocol, tool registry, sandbox timeouts, tiered
installer, health probe). Rust-side `SecDaemonManager`, `/security` panel
requiring explicit enable + an on-screen authorized-use acknowledgment before
the panel arms, `sec_*` tools harness-exempt per koma's own design (explicit
user-invoked, not silent). Tool roster ported 1:1 from the reference inventory
(http, sqlmap, nuclei, ffuf, dalfox, zap, xss-confirm, z3, sage, rsa, factordb,
lattice, hashcat, hashid, decode, jsdeobf, unmin, sourcemap, wasm, triage, rop,
pwntmpl, remote-session).
- **Mirror**: `src-security/koma_sec_daemon/*` end to end.
- **Validate**: Health probe correctly reports install status per tool; a
representative tool from each category (web/crypto/re/pwn) round-trips a real
call against a local authorized test target (e.g., a deliberately vulnerable
local CTF binary/webapp, not a live third-party system).
### Phase 13 — Internet module (search/fetch/download + full browser mode)
- **Action**: Lightweight in-process `web_fetch`/`web_search` (reqwest + html2md,
no Python dependency) plus opt-in `internet-sidecar/` Python package
(Playwright-Firefox scraping) for pages that resist the lightweight path,
invoked as a stateless one-shot subprocess per call with a hard timeout and
fallback to the lightweight path on any failure. `web_download` carries the
500 MiB cap and is in the catastrophic-op-guard-adjacent risky set (still no
approval prompt in Auto/Yolo, but still subject to the destructive-target guard
if it targets a path outside the workspace).
- **Mirror**: `tool/internet/*.rs`, `src-internet/scrapion_agent/*`.
- **Validate**: Fetch a JS-heavy page that fails the lightweight path, confirm
automatic fallback to the browser sidecar succeeds and returns clean markdown.
### Phase 14 — Remaining UI surface (settings, agents editor, MCP browse, usage
dashboard, help, effort picker, message rewind, quit confirm, session hub,
`/learning` dashboard)
- **Action**: Fill out the remaining `Mode` variants and their views/controllers
so the app matches koma's full UI surface, not just chat. Add the new
`/learning` dashboard mode (no koma equivalent — new for zesdex's Phase 7
feature set): lists every `lesson` memory (project + global, clearly
separated), sortable by trigger count / `confidence` / `stale` flag / recency,
with inline `forget`, manual `project → global` promotion, and manual
lesson-graduation/de-graduation actions reusing the same primitives Phase 7
built; a per-lesson detail view resolves its `provenance` back to the
originating session/turn and renders its structured `before`/`after`
snippet when present; a shadow-mode lesson's row shows its live
would-have-fired counter and trial-window progress; a separate list browses
`type: retrospective` memories by session; `/lesson export`/`import` are
also reachable as dashboard actions (not just slash commands), for
discoverability; the existing usage dashboard mode gains a `self-learning`-
tagged line item alongside the main agent's own token spend plus the three
quality-trend charts (build/test pass-rate, escalation count, per-lesson
trigger count) and the trigger-without-improvement `forget`-candidate
flagging (Phase 7 already tags/produces all of this data — this phase only
needs to render it).
- **Mirror**: `app/mode/*.rs`, `view/*.rs` per-mode files (`/learning` has no
koma source — built directly on Phase 7's `model::memory` extensions and the
existing `session_hub`-style list-mode view pattern for the layout shell);
koma's existing usage-dashboard view (extended, not replaced, for the new
cost category and trend charts).
- **Validate**: Manual pass through every mode transition documented in the
reference's mode state machine; no dead-end mode with no Esc/back path;
`/learning` correctly reflects a lesson's live `confidence`/`stale`/`scope`
state and a `forget` performed from the dashboard is picked up by the next
system-prompt rebuild; a lesson's provenance link correctly resolves to its
originating turn; a shadow-mode lesson's dashboard row updates live as its
trial counter increments; the usage dashboard's `self-learning` line item
and trend charts sum/plot correctly against a synthetic session history.
## Validation (run after every phase, not just at the end)
```sh
cargo build --workspace
cargo clippy --workspace -- -D warnings
cargo test --workspace
cargo run -- # manual smoke: start, drive one full tool-use turn, quit cleanly
```
## Risks
| Risk | Likelihood | Mitigation |
|---|---|---|
| "Zero comments, self-documenting" collides with genuinely non-obvious logic (e.g. cache-split marker placement, hysteresis thresholds in short-send) | Medium | Push complexity into well-named functions/types instead of comments; where a magic constant needs justification, name the constant itself descriptively rather than adding a comment |
| Security toolkit scope creep into unauthorized-use tooling | Low, given your stated authorized-use context | Phase 12 ships behind an explicit enable + on-screen acknowledgment, and the plan does not add any capability beyond the reference's own curated roster |
| "Full 1:1 clone" is a multi-week effort disguised as one `/plan` | High if treated as one PR | Plan is explicitly phased; each phase is independently buildable/testable and merges before the next starts — flag now if you want a smaller first slice |
| IPC/daemon split (Phase 11) is the highest-complexity phase and has the most failure modes (stale sockets, snapshot divergence) | Medium | Scheduled late, after the single-process TUI is fully proven, so the daemon split only has to preserve already-working behavior, not debug it simultaneously |
| Catastrophic-op guard list drifts out of sync as new risky tools are added later | Medium | Guard membership is a single named function/table (mirroring `tool_is_risky`), not scattered checks — Phase 3 establishes this as the one place to extend |
| Model over-invokes or under-invokes `workflow_run` (fan-out for trivial asks, or never fans out for genuinely decomposable work) since the trigger is judgement-based, not rule-based | Medium | Phase 6's validate step includes an explicit "trivial request stays inline" smoke test; system-prompt guidance is iterated on with real usage rather than assumed correct on first write |
| `workflow_run`'s embedded script surface (`agent`/`parallel`/`pipeline`) becomes a de facto second scripting language to maintain if scope creeps beyond the four primitives | Medium | Primitives are fixed at four for v1 (`agent`, `parallel`, `pipeline`, `phase`+`log` as cosmetic); no general control flow, no arbitrary host-function calls beyond spawning agents |
| Reviewer subagent produces low-signal or wrong "lessons" (hallucinated conventions, misjudged reasons) that then get injected into every future system prompt, actively degrading quality instead of improving it | Medium | Reviewer is read-only (can't touch files, only `remember`), every `remember` requires an explicit `recall`-first dedup pass, memory entries stay human-editable/deletable (`forget`) at any time, and the plan's acceptance bar requires the verdict note to be visible in-session so a bad lesson is easy to spot and remove, not silently compounding |
| Reasoned-edit ledger (`edits.jsonl`) grows unbounded over a long-lived project, and the reviewer queue (capacity 1) backs up during a burst of many small edits | Low for v1 scope | Ledger rotation/compaction is explicitly out of scope for v1 (flag as a fast-follow); the capacity-1 reviewer queue is a deliberate backpressure choice — reviews are best-effort and coalescing/skipping under load is acceptable, unlike the ledger itself which must never drop a line |
| A bad `global`-scope lesson pollutes every future project's system prompt, unlike a project-scope mistake which stays contained | Medium | Promotion to `global` is gated behind either explicit human calibration or the multi-project repeated-violation counter — never a single reviewer's own judgement — and `/learning` (Phase 14) gives an always-available manual `forget` path for any global lesson |
| Outcome-linked build/test verification adds real wall-clock cost to every file-touching turn, and a slow/flaky project test suite could make the reviewer trigger feel laggy or noisy | Medium | Verification runs on its own thread off the critical path (the user's turn is never blocked waiting on it), is capped at a short timeout, and degrades gracefully to reasons+diff-only review on timeout/failure/misconfiguration rather than blocking the reviewer entirely |
| Batch/offline pattern mining and the per-turn reviewer could in principle compound into runaway subagent spawning on a very active session | Low | Batch mining is explicitly rate-limited to at most once per idle window and shares the same capacity-1 review queue as the per-turn reviewer, so the two can never run concurrently |
| A graduated lesson's `check` rule is a blunt substring/glob match, not real language-aware parsing — it can misfire on a legitimate edit that superficially matches the pattern | Medium | A failed check never blocks the write outright; it returns a naming result the model can address or explicitly override via `reason`, and de-graduation is always available from `/learning` if a rule proves too blunt in practice |
| Two-reviewer consensus for automatic `global` promotion doubles the token cost of that specific path, and both reviewers could still share the same blind spot if spawned from an identical prompt | Low | The cost only applies to the rare `global`-promotion path, not routine per-turn review; the two reviewers are spawned independently (fresh context each) specifically so correlated blind spots are less likely than a single pass, though not impossible — flagged as a known limitation, not a guarantee |
| `/lesson import` bulk-extracting from a large convention document could flood the calibration queue with many pending entries at once | Low | Imported entries land at `confidence: opinion` and go through the same calibration toast as any reviewer-authored lesson — no special bypass — so the existing keep/discard/auto-resolve mechanics apply unchanged, just at higher volume for that one command |
| Shadow-mode trial window adds a delay before a genuinely useful graduated check becomes protective, during which the same real mistake could recur uncaught | Low | The trial window is a small fixed count by design (not open-ended), and the underlying `lesson` itself is still actively injected into the system prompt as `opinion`/`verified` text throughout the trial — shadow mode only gates the *hard enforcement* layer, not the softer prompt-injection signal, so protection is never fully absent during the trial |
| `note_finding`'s run-scoped broadcast could be misused as a general side-channel for workflow subagents to coordinate beyond its intended "share a quality finding" purpose, growing into an unbounded ad hoc messaging primitive | Medium | Scoped to exactly one tool, one direction (broadcast, not point-to-point), ephemeral (never persisted, dies with the run), and delivered only at existing tool-round boundaries (no new scheduling primitive) — the same "fixed primitives, no scope creep" discipline already applied to `workflow_run`'s four script primitives |
| Adaptive throttling could under-review a genuinely quiet-but-still-risky stretch of edits if the backoff schedule grows too aggressive | Low | Backoff resets to zero immediately on any new lesson/escalation/check-match, and the cap on how far it can back off is a fixed named constant (same "one place to extend" discipline as the catastrophic-op guard), so the maximum possible under-review window is always a known, bounded quantity, not unbounded drift |
| Quality trend metrics could be over-interpreted as proof the self-learning loop "works" when the underlying signals (pass-rate, escalation count) can move for unrelated reasons (e.g. the project itself just got harder) | Low | Charts are presented as raw trend data for human interpretation in `/learning`, not as an automated pass/fail verdict on the feature itself — the `forget`-candidate flag is a nudge for human review, not an automatic deletion |
| Lesson-pack export could leak project-specific naming/paths embedded in a `before`/`after` snippet when shared with a team or another machine, unlike prose-only lessons | Low | Export is opt-in and explicit per invocation (never automatic), scoped to `lesson`/`retrospective` types only, and the user reviews the exported file (plain readable frontmatter+markdown, same format as any other memory file) before sharing it — same trust model as sharing any other project file |
## Acceptance
- [ ] Phase 1 skeleton builds, runs, renders the exact 3-region layout + status
bar text specified, and restores the terminal cleanly on quit
- [ ] `Cargo.toml` matches the specified name/authors/dependency set exactly
- [ ] No `unimplemented!()`, `todo!()`, `/* implement later */`, or equivalent
placeholder in any file belonging to a "done" phase
- [ ] No `//`, `///`, or `/* */` comments anywhere in `src/`
- [ ] No `#![allow(dead_code)]` / `#![allow(unused_variables)]` or per-item
`#[allow(...)]` suppressions
- [ ] Auto/Yolo mode performs routine writes/edits/shell/git with zero approval
prompts and zero added latency
- [ ] Catastrophic-op guard blocks its named operation list even in Yolo mode,
verified by unit test per rule
- [ ] Security toolkit is inert until explicitly enabled, and the enable path
surfaces an authorized-use acknowledgment
- [ ] `workflow_run` is the sole subagent-delegation surface (no separate
single-agent `task` tool coexists with it)
- [ ] Workflow Progress panel auto-appears on `workflow_run` start, is
dismissible without killing the run, and is re-summonable
- [ ] `pipeline()` demonstrably has no inter-stage barrier and `parallel()`
demonstrably respects the concurrency cap, each covered by its own test
- [ ] `write`/`edit` calls without a non-empty `reason` are rejected before any
filesystem mutation, verified by unit test for both tools
- [ ] Every accepted `write`/`edit` produces exactly one `edits.jsonl` line with
a matching `path`/`reason`/`origin`
- [ ] Exactly one detached reviewer subagent runs per turn that touched files
(never zero when edits happened outside Plan mode, never more than one
concurrently), and its verdict appears as a collapsed system note that is
never appended to the model-visible transcript
- [ ] A reviewer's `remember(type="lesson")` calls are demonstrably dedup'd
against existing lessons via a `recall`-first check (no duplicate memory
entries from repeated similar observations)
- [ ] Lessons are labeled `confidence: verified` only when backed by a
reproduced build/test failure, `opinion` otherwise, and the two are
visibly distinguished everywhere lessons are listed
- [ ] A contradictory new lesson blocks auto-save and surfaces both entries in
the calibration toast instead of overwriting silently
- [ ] Pending lessons auto-resolve to `keep` in Auto/Yolo after a grace window
(staying visible/reversible) and require an explicit keypress in Normal
mode
- [ ] A `scope: project` lesson never leaks into a different project's
injected memory index; a `scope: global` lesson appears in every project
- [ ] A lesson pattern repeated a third time within the configured window
produces a non-collapsed escalation note distinct from routine verdicts
- [ ] `/learning` dashboard (Phase 14) lists every lesson with live
confidence/stale/scope state and supports `forget` + manual promotion
- [ ] The build/test verification probe resolves correctly for at least three
distinct project-ecosystem fixtures with no ecosystem hard-coded outside
the one named probe table (language-agnostic requirement)
- [ ] A graduated lesson's `check` fires on a violating `write`/`edit` as a
named, addressable/overridable result — never a silent or unbreakable
block
- [ ] Every `lesson` resolves its `provenance` back to a real session/turn (or
`"user"` for a `/lesson`-authored entry), verified from `/learning`
- [ ] Exactly one `type: retrospective` memory is produced per ended session
and is absent from the next session's injected system-prompt index
- [ ] `/lesson <text>` creates a `confidence: human` entry with no calibration
delay; `/lesson import <path>` seeds `opinion`-confidence entries that DO
go through calibration
- [ ] Automatic (non-`/lesson`) promotion to `global` scope requires two
independently-spawned reviewers to agree, verified by a test where only
one agrees and the promotion is blocked
- [ ] The usage dashboard (Phase 14) shows a distinct `self-learning` token-
cost line item, separate from the main agent's own spend
- [ ] A `check` candidate spends its full trial window in `shadow: true`
before ever blocking/naming anything to the model, and correctly
graduates or demotes at window close based on its fire ratio
- [ ] `note_finding` calls inside a `workflow_run` are observable by sibling
`agent()` thunks in the same run and are confirmed absent from
`MEMORY.md` afterward (ephemeral, run-scoped only)
- [ ] Consecutive empty review passes measurably reduce review frequency, and
a single subsequent lesson/escalation/check-match resets it to zero
- [ ] Usage dashboard trend charts (pass-rate, escalation count, per-lesson
trigger count) plot correctly against a synthetic session history, and
a high-trigger/no-improvement lesson is flagged as a `forget` candidate
- [ ] A lesson with a structured `before`/`after` snippet renders it in both
`/learning` and its injected system-prompt text
- [ ] `/lesson export` followed by `/lesson import` on a separate project
reproduces the same lesson set with matching confidence/scope/
provenance, and a near-duplicate import is caught by the existing dedup
path
- [ ] Each phase's own validation commands pass before the next phase starts
**WAITING FOR CONFIRMATION**: Proceed with this plan? (yes/no/modify — you can
also ask to start at a specific phase, e.g. "just do Phase 1-3 for now")