# Architecture — Jovaltus Jovaltus is a Hermes plugin that implements a **subagent-driven deterministic development framework**. The plugin ships 4 tools (`plan`, `execute`, `simplify`, `review`) whose handlers dispatch isolated subagents via Hermes's `subagent_lifecycle`; a plugin-owned state machine (`state.py`, JSON-persisted for cross-session resume) records every phase transition; and 4 hooks (`subagent_start`, `subagent_stop`, `pre_llm_call`, `post_llm_call`) drive the chain forward deterministically and inject pipeline status into the main agent's context every turn. The main agent does NOT decide pipeline flow — it calls tools and reads status. The plugin also bundles **5 utility skills** (`agentic-debugging`, `manage-agents-md`, `manage-git-repo`, `project-documentation`, `qa`) that pipeline subagents load for specific jobs. The 4 tools, state machine, and hooks are implemented in Python; the skills are independent Hermes skills. ## System Context (C4 Level 1) ```mermaid graph TD Agent[Main Agent] -->|calls tool| Tools[4 Plugin Tools
plan / execute / simplify / review] Tools -->|subagent_lifecycle.launch| Sub[Pipeline Subagents] Hooks[Hooks
subagent_start / subagent_stop / pre_llm_call] -->|advance chain| State[State Machine
state.py + jovaltus_state.json] State -->|status line| Agent Sub -->|write artifacts| RunDir[.plan/<YYYYmmdd>/<plan_name>/] Sub -->|read/write| Repo[Git Repository] ``` **Users:** A main agent (human + LLM) that calls the 4 tools and reads the hook-injected status line. **External services:** | Service | Purpose | Protocol | |---------|---------|----------| | Hermes Agent Runtime | Host process; calls `register(ctx)` at startup | Python in-process | | LLM Provider | Powers main agent + pipeline subagents | HTTP API | | Git Repository | Source of truth; `execute` leaves the diff uncommitted for simplify/review | git CLI | | `~/.hermes/jovaltus_state.json` | Persisted pipeline state (cross-session resume) | JSON via `fabricium.state` | ## Container View (C4 Level 2) ```mermaid graph TD Agent -->|plan / execute / simplify / review| Tools[Tool Handlers
tools.py] Tools -->|start_pipeline + dispatch| State[state.py] Tools -->|delegate_task| Sub[Pipeline Subagent] Hooks[Hooks
hooks.py] -->|complete_child + dispatch next| State Hooks -->|delegate_task| Sub Prompts[prompts/*.md] -->|goal text| Tools Prompts -->|goal text| Hooks Fabricium[Fabricium SDK] -->|HermesPlugin| CLI[CLI Commands] ``` | Container | Technology | Purpose | |-----------|-----------|---------| | Tool Handlers (`tools.py`) | Python | Validate args, start pipeline, dispatch first-phase subagent; `execute` also checks `delegation.max_spawn_depth` | | State Machine (`state.py`) | Python + JSON | `PipelineState` dataclass; phase/status/child/verdict bookkeeping; persisted under the `"pipeline"` key | | Hooks (`hooks.py`) | Python | `subagent_start` associates children, `subagent_stop` advances the chain, `pre_llm_call` injects status | | Subagent Prompts (`prompts/`) | Markdown | 9 goal documents, one per phase, with `[[token]]` placeholders | | Bundled Skills (`skills/`) | Markdown (SKILL.md) | 5 utility skills loaded by subagents | | CLI Commands | `hermes jovaltus setup\|status\|update` | Profile management + skill installation (fabricium) | ## Plugin Architecture The registration flow (Contract §6) is in `src/jovaltus/__init__.py` (`register()`, lines 46-67): ```python def register(ctx): plugin.register(ctx) # fabricium: CLI + bundled skills (line 56) jovaltus_tools.register(ctx) # 4 tools (line 61) jovaltus_hooks.init(ctx) # capture ctx for hooks (line 62) ctx.register_hook("subagent_start", hooks.on_subagent_start) # line 63 ctx.register_hook("subagent_stop", hooks.on_subagent_stop) # line 64 ctx.register_hook("pre_llm_call", hooks.on_pre_llm_call) # line 65 ``` **What Fabricium handles** (`plugin.register(ctx)`): CLI command registration (`setup`, `status`, `update`, `update --check`) and bundled skill auto-discovery from `src/jovaltus/skills/`. **What the plugin adds** on top of fabricium: - **4 tools** (`tools.register`, `src/jovaltus/tools.py:85-140`): `plan` (requires `user_requirements`), `execute` (requires `plan`), `simplify` / `review` (take an optional `plan` — they can run against the uncommitted changes directly). All registered with `toolset="jovaltus"`, `is_async=False`; handlers are `handler(args, **kwargs) -> str` returning JSON (`{"status":"started"|"error", ...}`). - **State machine** (`src/jovaltus/state.py`): `PipelineState` dataclass (lines 53-94), `PHASES` (24-34), `STATUSES` (36), and the API in `get_pipeline` / `start_pipeline` / `set_phase` / `register_child` / `complete_child` / `set_verdict` / `finish_pipeline` / `status_text` / `reset_pipeline`. - **4 hooks** (`src/jovaltus/hooks.py`): `on_subagent_start` (49-77), `on_subagent_stop` (79-106), `on_pre_llm_call` (108-122), `on_post_llm_call` (124-165). ## State Machine and Phase Chains Pipeline state lives in `~/.hermes/jovaltus_state.json` under the top-level `"pipeline"` key (the `"profiles"` key is fabricium-owned and never touched) — `src/jovaltus/state.py:134-149`. Every transition is persisted, so an interrupted pipeline resumes on the next session via `get_pipeline()` (`state.py:151-179`). `get_pipeline()` is self-healing across plugin upgrades: - A pipeline persisted by v1.1.2 (phase `simplify_fix` / `review_fix`) is migrated to the v1.1.3+ waiting phases (`simplify_waiting` / `review_waiting`) — the new CHAIN has no fixer-phase keys, so without the migration the hooks would KeyError and strand the loop (`_LEGACY_FIXER_PHASES`, `state.py:59-66`). - A phase the CHAIN does not know at all (corrupted/foreign state) is auto-cleared back to idle (`reset_pipeline`) rather than deadlocking `CHAIN[tool][phase]`. Phase sequences (chain table `CHAIN` in `src/jovaltus/tools.py:163-171`): | Tool | Chain | Terminal | |------|-------|----------| | `plan` | prd → research → acceptance → tasks | done (produces `tasks.md` DAG manifest) | | `execute` | execute | done | | `simplify` | simplify ⇄ simplify_waiting (verdict-driven loop) | done on `"pass"` verdict | | `review` | review ⇄ review_waiting (verdict-driven loop) | done on `"pass"` verdict | `PHASES` (`state.py:24-34`): `prd`, `research`, `acceptance`, `tasks`, `execute`, `simplify`, `simplify_waiting`, `review`, `review_waiting`. ### How a chain advances 1. A tool handler validates its args, calls `start_pipeline(tool, run_dir, ...)` (`state.py:181-216`), and dispatches the first phase via `dispatch_pipeline_step` (`tools.py:280-318`) — which loads the phase's prompt, substitutes `[[token]]` placeholders with `str.replace` (`tools.py:340-352`), and calls `subagent_lifecycle.launch(SubagentLaunchRequest(goal=..., context=..., "role": "orchestrator" | "leaf"})`. The `role` is `"orchestrator"` only for the execute phase. 2. `subagent_start` fires when the child spawns: `on_subagent_start` (`hooks.py:49-77`) matches the child's goal against the `[jovaltus-pipeline::]` marker and records the child as the pipeline's active child via `register_child` (`state.py:227-232`). No marker match → no-op (orchestrator grandchildren, foreign children, and user-initiated subagents never touch pipeline state). 3. `subagent_stop` fires when the child completes: `on_subagent_stop` (`hooks.py:79-106`) calls `complete_child` (`state.py:234-257`) — True only for the active child. A non-`"success"` status fails the pipeline (`finish_pipeline(ok=False, error=summary)`); otherwise `_advance` (`hooks.py:167-206`) follows the chain: - simplify/review reviewer phases: reads `/verdict.json` (`hooks.py:337-357`); `"pass"` → `set_verdict` + `finish_pipeline(ok=True)`; `"fix"` → increment `loop_iteration`, set verdict, park the pipeline in the `*_waiting` phase (`_waiting_phase`, `hooks.py:208-215`) and push a fix-request event (`_push_fix_request_event`, `hooks.py:303-320`) that wakes the main agent with the findings. **No fixer subagent is dispatched** — the main agent performs the fixes (it has no subagent iteration cap and full conversation context). **No iteration cap.** - Other phases: `set_phase(next)` (`state.py:218-225`) + dispatch the next phase's subagent. `next_phase == "done"` → finish with `ok=True`. 4. `pre_llm_call` fires before every main-agent turn: `on_pre_llm_call` (`hooks.py:108-122`) returns `{"context": status_text(p)}` when a pipeline exists, else `None` — the status line (`[Jovaltus pipeline] tool=... phase=... status=... run_dir=...`, `state.py:279-292`) is injected into the user message so the main agent always sees pipeline state. 5. `post_llm_call` fires after every completed agent turn: `on_post_llm_call` (`hooks.py:124-165`) re-dispatches the reviewer when the pipeline is parked in a `*_waiting` phase and the completed turn belongs to the main agent of the owning session (`platform != "subagent"` and, when the pipeline carries a `session_key`, the turn's `session_id` matches it). This is how the loop re-runs the review after the main agent finishes fixing — and it is inert once the pipeline is `done`/`failed`, so the hook effectively does not exist outside the loop. ## Tool Details All four handlers live in `src/jovaltus/tools.py` and return a JSON string (Contract §1): `{"status":"started","tool":...,"phase":...,"run_dir":..., "message":...}` on success, `{"status":"error","message":...}` on invalid input (e.g. missing `plan` path for `execute`, nonexistent plan file). | Tool | Handler | Input | Behavior | |------|---------|-------|----------| | `plan` | `plan_handler` (`tools.py:446-465`) | `user_requirements` (required) | Computes run dir `/.plan///` (`tools.py:598-615`; repo root inherited from the main agent via `resolve_agent_cwd()` — per-session cwd → `TERMINAL_CWD` → process cwd, `tools.py:404-426`; plan_name = kebab-case of first ~6 words, `-2`/`-3`… suffix on collision); creates the run dir; starts pipeline phase `prd` | | `execute` | `execute_handler` (`tools.py:468-482`) | `plan` (required, must exist) | Precondition: effective `delegation.max_spawn_depth >= 2` (`tools.py:629-644`), else returns error; starts pipeline phase `execute` with `role="orchestrator"` | | `simplify` | `simplify_handler` (`tools.py:485-501`) | `plan` (optional, must exist when given) | With a plan the run dir is the plan's parent; without one a fresh `.plan//simplify` dir is created and the uncommitted diff is simplified standalone. Starts pipeline phase `simplify` (reviewer) | | `review` | `review_handler` (`tools.py:504-520`) | `plan` (optional, must exist when given) | With a plan the run dir is the plan's parent; without one a fresh `.plan//review` dir is created and the uncommitted diff is reviewed standalone. Starts pipeline phase `review` (reviewer) | The repo root is passed to children twice: as a `[[repo_root]]` token in every prompt (`tools.py:357-392`, substituted with the main agent's working dir from `_repo_root()`), and inside the `context` text (`tools.py:394-401`) — never as a tool parameter (the `delegate_task` handler ignores `workspace_path`/`max_spawn_depth`). When a pipeline reaches a terminal state (done or failed), `subagent_stop` pushes a completion event onto the shared `process_registry.completion_queue` (`hooks.py:217-272`) — the same rail background terminal tasks use — so the desktop/TUI, CLI, and gateway surfaces wake the main agent with a "pipeline complete" turn instead of leaving it silent until the user's next message. Routing metadata (session key, UI session id) is captured per-run on the main-turn dispatch (`tools.py:94-121`), persisted on the pipeline (`state.py:94-97`), and read back by the event builders — so each run's notifications are addressed to the session that started it, never a process-global snapshot that a parallel session could clobber (v1.1.5 fix). The hooks also gate on the owning session: `on_subagent_start` / `on_subagent_stop` require `parent_session_id == p.session_key` and `on_post_llm_call` requires the turn's `session_id == p.session_key` when the pipeline carries a session key. Every subagent reads the repository first. Each prompt's Step 0 instructs the child to explore `[[repo_root]]` — `AGENTS.md`/`CLAUDE.md`, the project manifest, source layout, and tests — before producing its artifact, so PRD/design/acceptance/tasks are grounded in real code and reviewers judge diffs in context. Greenfield repos (no relevant code) are handled explicitly in each prompt. **Subagents share the main agent's toolset.** `SubagentLaunchRequest` leaves `allowed_toolsets` unset (`tools.py:320-338`), so Hermes child construction inherits the parent's enabled toolsets (`delegate_tool.py:1392-1395`) instead of restricting children to a fixed list — the same file/terminal/web tools that let the main agent read the repo are available to every pipeline subagent. ## Subagent Prompts (`prompts/`) `src/jovaltus/prompts/` is a Python package (`PROMPT_NAMES`, `__init__.py:11-19`; `load_prompt`, `__init__.py:24-39` — raises `FileNotFoundError` for unknown names). Each of the 7 Markdown files is the goal document for one phase's subagent: | File | Phase | Artifact written to `run_dir` | |------|-------|-------------------------------| | `prd.md` | prd | `prd.md` | | `research.md` | research | `design.md` | | `acceptance.md` | acceptance | `acceptance.md` | | `tasks.md` | tasks | `tasks.md` (task DAG manifest: serial / batch / fully-parallel forms + mermaid DAG) | | `execute.md` | execute | none — orchestrator drives the DAG level by level, **no git commits** (diff left for simplify/review) | | `simplify-review.md` | simplify | `verdict.json` `{"verdict":"pass"\|"fix","findings":"…"}` | | `review.md` | review | `verdict.json` (adversarial review findings) | There are no fixer prompts: a `"fix"` verdict parks the pipeline in the `*_waiting` phase and the MAIN agent fixes the findings directly (no subagent iteration cap); `on_post_llm_call` then re-dispatches the reviewer. Token substitution is `str.replace` on `[[token]]` (`tools.py:340-352`) — never `.format()`, because prompt bodies contain mermaid `{}` braces. Tokens are `[[run_dir]]`, `[[repo_root]]`, `[[user_requirements]]` (prd only), and `[[plan_path]]` (execute/simplify/review phases). Every prompt also carries the literal marker `[jovaltus-pipeline:TOOL:PHASE]` which the dispatcher replaces with the real `[jovaltus-pipeline::]` marker used by `subagent_start` (`tools.py:144-146`). ## Bundled Skills (5 utility) Fabricium auto-discovers skills from `src/jovaltus/skills/` (one directory per skill, each with `SKILL.md`). The 5 bundled skills are independent Hermes skills — they are NOT pipeline phases and the state machine never navigates them: | Skill | Purpose | |-------|---------| | `agentic-debugging` | Evidence-driven debugging for bugs, errors, crashes, test failures | | `manage-agents-md` | Create, audit, update agent specification files (AGENTS.md, CLAUDE.md, ...) | | `manage-git-repo` | Git repository management: commits, semantic-version releases, branches + PRs | | `project-documentation` | Generate a structured multi-file docs/ tree from any codebase | | `qa` | Standalone PRD-driven acceptance testing across all app types | ## Key Architectural Decisions | Decision | Rationale | Status | |----------|-----------|--------| | Subagent-driven, deterministic framework | Main agent calls tools and reads status; the state machine + hooks decide flow | Active | | State machine with JSON persistence | Cross-session resume; deterministic transitions; plugin-owned (`"pipeline"` key, `"profiles"` untouched) | Active | | 4 hooks wire subagent lifecycle | `subagent_start` associates, `subagent_stop` advances, `pre_llm_call` injects status, `post_llm_call` re-dispatches the reviewer after the main agent's fixing turn | Active | | Main agent fixes, not a fixer subagent | A fixer leaf subagent shares the 50-iteration delegation cap and gets cut off mid-fix on large findings (observed 2026-08-08: 16 iteration-capped rounds); the main agent has no such cap | Active | | No iteration cap on simplify/review loops | The verdict file is the loop's exit condition; the main agent fixes until the reviewer passes | Active | | Per-session routing + parent scoping | Notifications and hook-driven re-dispatches attach to the session that STARTED the run (`PipelineState.session_key` / `origin_ui_session_id`, per-session parent cache), so parallel main-agent sessions in one gateway process never receive or drive each other's pipelines (fixed v1.1.5) | Active | | `execute` leaves the diff uncommitted | simplify/review operate on the working tree diff, not commits | Active | | Fabricium as the only runtime dependency | Avoids duplicating git wrappers, CLI registration, skill bundling, and state persistence | Active | | Self-bootstrap fabricium on import | Hermes may recreate its venv, dropping plugin deps; repair on first import | Active | ## Deployment Jovaltus is distributed as a pip-installable Hermes plugin via PyPI (trusted publisher). ``` CI/CD → git tag → PyPI trusted publisher → pip install jovaltus ``` `hermes jovaltus setup` creates the `jovaltus-agent` profile, installs the bundled skills, and optionally applies `SOUL.md`. ## How to Update - New tool added? → Update Tool Details table + registration flow - Phase chain changes? → Update the chain table (source of truth: `CHAIN` in `src/jovaltus/tools.py`) - New prompt added? → Update the prompts table + `PROMPT_NAMES` - Hook behavior changes? → Update the "How a chain advances" section - Skill added/removed? → Update Bundled Skills table - Plugin API changes? → Update Plugin Architecture section - Fabricium API changes? → Update Container View ## Find It Fast ```bash cat src/jovaltus/__init__.py # register() flow (Contract §6) cat src/jovaltus/state.py # State machine (PHASES, STATUSES, API) cat src/jovaltus/tools.py # 4 tool handlers + CHAIN + dispatch cat src/jovaltus/hooks.py # 4 hook callbacks ls src/jovaltus/prompts/ # 7 subagent goal prompts ls src/jovaltus/skills/ # 5 bundled utility skills ```