Initial commit: hqt — HQ Terminal TUI

TUI for orchestrating AI coding harness sessions (Claude Code, Codex,
Kiro, etc.) via tmux. Click CLI bootstraps a Textual TUI over
ProjectService/SessionService backed by SQLite, spawning harness
sessions as tmux windows through TmuxManager.

Includes recent fixes:
- Visible Tab focus highlight on dialog OK/Cancel buttons
- Auto-select first project on launch
- Auto-select first session + per-project session-selection memory
- tmux new-window targets an explicit free index, fixing
  "index N in use" failures (broken spawn/attach in attached sessions)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 17:06:57 -04:00
commit 90944948f5
61 changed files with 8834 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
# HQ Terminal — Implementation Plan
Reference spec: `.hq/2026-06-09-spec-hq-terminal.md`
## Phase 1: Project Scaffold
Set up the new workspace with tooling, dependencies, and basic structure.
### Steps
1. **Create workspace** at `~/dev/hqt/` with `uv init --lib --name hqt`
2. **Configure pyproject.toml**:
- Python ≥ 3.12
- Dependencies: `textual>=2.0`, `sqlalchemy>=2.0`, `click>=8.0`
- Dev dependencies: `pytest`, `pytest-asyncio`, `ruff`
- Entry point: `[project.scripts] hqt = "hqt.cli:main"`
3. **Create package structure** per spec: `src/hqt/` with all subdirectories
4. **Create `.hq/PROJECT.md`** for the new project
5. **Verify**: `uv run hqt --help` produces output
## Phase 2: Database Layer
SQLite models, engine, and schema management.
### Steps
1. **`src/hqt/config.py`** — Settings dataclass with paths:
- `db_path`: `~/.local/share/hqt/hqt.db`
- `config_dir`: `~/.config/hqt/`
- `skills_dir`: `~/.config/hqt/skills/`
- `tmux_path`: `tmux`
- `tui_session_name`: `hqt-main`
2. **`src/hqt/db/models.py`** — SQLAlchemy models: `Project`, `Harness`, `Session`, `McpServer`, `ProjectMcpServer`, `ProjectSkill`
3. **`src/hqt/db/session.py`** — Engine factory, session factory, `ensure_db()` (creates dir + tables)
4. **`src/hqt/db/migrate.py`** — Additive migration helper (add missing columns)
5. **Tests**: model creation, ensure_db on fresh + existing databases
6. **Verify**: `uv run pytest tests/test_db.py -q` passes
## Phase 3: TmuxRunner
The async tmux command abstraction. Largely ported from HQ.
### Steps
1. **`src/hqt/tmux/runner.py`** — Port `TmuxRunner` from HQ:
- `new_session(name, cwd, command, width, height)`
- `has_session(name) -> bool`
- `kill_session(name)`
- `send_text(name, text)`
- `send_enter(name)`
- `capture_pane(name) -> str`
- `switch_client(target)` ← NEW (not in HQ)
- `list_sessions() -> list[str]` ← NEW
2. **`src/hqt/tmux/manager.py`** — Higher-level session lifecycle:
- `spawn_session(session: Session, config: SpawnConfig) -> None`
- `resume_session(session: Session, config: SpawnConfig) -> None`
- `kill_session(session: Session) -> None`
- `attach_session(session: Session) -> None` (switch-client)
- `is_alive(session: Session) -> bool`
- `poll_all_sessions(sessions: list[Session]) -> dict[int, bool]`
3. **Tests**: mock `_exec` function, verify command construction
4. **Verify**: `uv run pytest tests/test_tmux.py -q` passes
## Phase 4: Harness Abstraction Layer
The configurator protocol and concrete implementations.
### Steps
1. **`src/hqt/harnesses/base.py`** — `HarnessConfigurator` protocol, `SpawnConfig`, `HarnessIdentity` dataclasses
2. **`src/hqt/harnesses/claude.py`** — Claude Code configurator:
- UUID generation via `uuid5`
- Spawn: `claude --session-id {uuid} --model {model}`
- Resume: `claude --resume {uuid}`
- MCP: write project `.mcp.json`
- Skills: write/append `CLAUDE.md`
- Ready/busy regexes from terminal profiles
3. **`src/hqt/harnesses/kiro.py`** — Kiro CLI configurator (stub for now):
- Basic spawn command
- MCP: Kiro-managed (no injection in v1)
4. **`src/hqt/harnesses/aider.py`** — Aider configurator:
- Spawn: `aider --model {model} --restore-chat-history`
- Skills: `--read {skill_file}` flags
- No MCP support
5. **`src/hqt/harnesses/generic.py`** — Fallback for unknown harnesses:
- Env-var based config only
6. **`src/hqt/harnesses/registry.py`** — Discover available harnesses (`shutil.which`), return configurator instances
7. **Tests**: each configurator produces correct SpawnConfig for given inputs
8. **Verify**: `uv run pytest tests/test_harnesses.py -q` passes
## Phase 5: Skills & MCP Services
Skill registry and MCP server management.
### Steps
1. **`src/hqt/skills/registry.py`** — Port from HQ: scan `skills_dir`, parse frontmatter, return `Skill` objects
2. **`src/hqt/skills/injection.py`** — Port from HQ: `wrap_prompt_with_skills()`
3. **`src/hqt/mcp/service.py`** — CRUD for MCP servers and project bindings (DB operations)
4. **`src/hqt/projects/service.py`** — Project CRUD: add, list, archive, git status reading
5. **Tests**: skill discovery with fixture directory, MCP CRUD, project CRUD
6. **Verify**: `uv run pytest tests/test_skills.py tests/test_mcp.py tests/test_projects.py -q` passes
## Phase 6: Session Service (Orchestration Core)
The service that ties together DB, tmux, and harness configurators.
### Steps
1. **`src/hqt/sessions/service.py`** — Session lifecycle:
- `create_session(project_id, harness_name, nickname, model, skill_names, mcp_ids) -> Session`
- Generates harness UUID via configurator
- Builds SpawnConfig
- Calls tmux manager to spawn
- Persists session row
- `resume_session(session_id) -> None`
- Loads session from DB
- Builds resume SpawnConfig with stored UUID
- Calls tmux manager to spawn
- `stop_session(session_id) -> None`
- Kills tmux session
- Updates DB (last_activity_at)
- `delete_session(session_id) -> None`
- Kills tmux if alive
- Removes DB row
- `attach_session(session_id) -> None`
- Calls tmux switch-client
- `list_sessions(project_id) -> list[SessionInfo]`
- Joins DB data with tmux liveness
2. **Tests**: mock tmux manager, verify session lifecycle
3. **Verify**: `uv run pytest tests/test_sessions.py -q` passes
## Phase 7: Textual TUI
The user interface.
### Steps
1. **`src/hqt/tui/app.py`** — Textual `App` subclass:
- Bindings: q=quit, n=new session, a=add project, /=filter
- Compose: Header, ProjectList, SessionList, Footer
- Timer: poll tmux liveness every 3s
2. **`src/hqt/tui/widgets/project_list.py`** — ListView of projects:
- Shows name, path, branch
- Selection updates session list
- Keybindings: Enter=select, a=add, d=archive
3. **`src/hqt/tui/widgets/session_list.py`** — ListView of sessions for selected project:
- Shows harness icon, nickname, alive/dead status
- Keybindings: Enter=attach, n=new, d=delete, r=rename, R=resume (if dead)
4. **`src/hqt/tui/screens/new_session.py`** — Modal screen:
- Harness selector (dropdown)
- Nickname input
- Model selector (populated from configurator.supported_models)
- Skills multi-select
- MCP servers multi-select
- Create / Cancel buttons
5. **`src/hqt/tui/screens/add_project.py`** — Modal:
- Path input (with directory completion if feasible, or paste)
- Name override (optional)
6. **`src/hqt/tui/styles.tcss`** — Textual CSS for layout
7. **Integration test**: App mounts, projects display, key bindings respond
8. **Verify**: `uv run pytest tests/test_tui.py -q` passes (headless Textual testing)
## Phase 8: CLI Entry Point & tmux Bootstrap
Wire everything together.
### Steps
1. **`src/hqt/cli.py`** — Click CLI:
- `hqt` (default) — bootstrap into tmux + launch TUI
- `hqt list` — print projects/sessions table
- `hqt attach <id>` — switch to session
- `hqt new <project> <harness>` — create session non-interactively
- `hqt doctor` — check tmux, harness availability
2. **`src/hqt/__main__.py`** — `python -m hqt` support
3. **tmux bootstrap logic**:
```python
def bootstrap():
if not in_tmux():
# Create hqt-main session with TUI as the command
exec tmux new-session -s hqt-main "hqt --inside-tmux"
else:
# Already in tmux, just run the TUI
run_tui()
```
4. **First-run setup**: suggest tmux keybinding, create config dir/skills dir
5. **Verify**: `hqt doctor` works, `hqt` launches TUI inside tmux
## Phase 9: Integration Testing & Polish
### Steps
1. **End-to-end test**: create project → create session → verify tmux session exists → attach → kill → verify dead
2. **Handle edge cases**:
- tmux not installed → clear error
- Harness not found → show in UI, disable create
- DB locked → retry pattern
- Session name collision → fail gracefully
3. **README.md** with quick start
4. **Verify**: full test suite passes, manual smoke test with claude-code
## Dependency Graph
```
Phase 1 (scaffold)
├── Phase 2 (database)
│ │
│ ├── Phase 5 (skills + MCP)
│ │ │
│ │ └──────┐
│ │ │
│ └── Phase 6 (session service) ◄── Phase 4 (harness abstraction)
│ │
├── Phase 3 (tmux runner) ──────────────┘
│ │
│ ▼
└── Phase 7 (TUI) ◄──────────── Phase 6
Phase 8 (CLI + bootstrap)
Phase 9 (integration + polish)
```
## Estimated Effort
| Phase | Complexity | Approx. LOC |
|-------|-----------|-------------|
| 1. Scaffold | Low | ~50 (config) |
| 2. Database | Low | ~150 |
| 3. TmuxRunner | Low | ~120 (mostly port) |
| 4. Harness abstraction | Medium | ~300 |
| 5. Skills + MCP | Low | ~150 (mostly port) |
| 6. Session service | Medium | ~200 |
| 7. TUI | Medium-High | ~400 |
| 8. CLI + bootstrap | Low | ~100 |
| 9. Integration | Medium | ~150 |
| **Total** | | **~1,620** |
## Implementation Order (sequential for a single agent)
1. Phase 1 → 2. Phase 2 → 3. Phase 3 → 4. Phase 4 → 5. Phase 5 → 6. Phase 6 → 7. Phase 7 → 8. Phase 8 → 9. Phase 9
Each phase produces a working, testable increment. The TUI (Phase 7) is the largest single piece but depends on everything below it being solid.
+542
View File
@@ -0,0 +1,542 @@
# HQ Terminal — Specification
## Vision
HQ Terminal (`hqt`) is a Textual-based TUI application that orchestrates AI coding harness sessions (Claude Code, Kiro CLI, Codex CLI, Aider, Copilot, etc.) using tmux as the session multiplexer. It replaces HQ's web-based dashboard with a terminal-native workflow where the user navigates projects and sessions in the TUI, then switches directly into harness TUIs running inside tmux.
## Core Workflow
1. User runs `hqt` — attaches to (or creates) a dedicated tmux session running the Textual TUI
2. TUI shows project list → user selects a project → sees its sessions
3. User selects an existing session → `tmux switch-client` puts them in the harness pane
4. User presses a hotkey (e.g., `Ctrl-b o` or custom prefix) → switches back to the TUI
5. User can create new sessions, specifying harness type, skills, and MCP servers
6. Sessions persist across TUI restarts (tmux keeps them alive)
## Architecture
```
┌──────────────────────────────────────────────────────────┐
│ Any terminal emulator │
└─────────────────────────┬────────────────────────────────┘
┌─────────────────────────┴────────────────────────────────┐
│ tmux server │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ hqt TUI │ │ claude-code │ │ kiro │ │
│ │ (Textual) │ │ (project-a) │ │ (project-b) │ │
│ │ │ └────────────────┘ └──────────────┘ │
│ │ • Projects │ ┌────────────────┐ │
│ │ • Sessions │ │ aider │ │
│ │ • Config │ │ (project-a) │ │
│ └──────────────┘ └────────────────┘ │
│ │
└───────────────────────────────────────────────────────────┘
```
## What Carries Over from HQ
### Reuse directly (copy or extract)
- **Skills system** — `SkillRegistry`, skill discovery from `~/.config/hq/skills/*/SKILL.md`, frontmatter parsing. Skills are injected as prefixes to prompts sent to harnesses.
- **MCP server definitions** — the concept of globally-defined MCP servers bound to projects. Stored in DB, serialized to harness-specific config formats at spawn time.
- **Terminal profiles** — `TerminalProfile` dataclass, `terminal_profiles.json` user overrides, `DEFAULT_TERMINAL_PROFILES`. These define how to spawn/resume each TUI harness.
- **TmuxRunner** — the async tmux command abstraction (new_session, has_session, send_text, send_enter, capture_pane, kill_session, etc.)
- **Project model concept** — name, path, git branch/dirty state.
### Reuse conceptually (reimplement simpler)
- **Session model** — simplified. No connection_status/activity_status two-axis state. A session is: exists in DB + optionally alive in tmux.
- **Harness registry** — discovery of available harnesses via `shutil.which`. Simplified to just terminal-profile harnesses (no JSON-RPC protocol adapters).
### Drop entirely
- **FastAPI / HTTP / WebSocket server** — no web interface
- **React frontend** — replaced by Textual
- **JSON-RPC adapters** (kiro_acp, codex_app_server, copilot_acp) — harnesses run as native TUIs
- **SessionEvent / append-only event store** — no event sourcing needed
- **Event broadcasting / supervisor / pump** — tmux IS the supervisor
- **Docs panel / todo panel** — user has direct terminal access to files
- **Session runs / turn tracking** — harness manages its own conversation state
## Data Model
SQLite database at `~/.local/share/hqt/hqt.db`.
### Projects
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK | |
| name | TEXT | Display name (default: directory basename) |
| path | TEXT UNIQUE | Absolute path to project root |
| created_at | TIMESTAMP | |
| last_opened_at | TIMESTAMP | For sort order |
| archived | BOOLEAN | Hide from default list |
### Harnesses
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK | |
| name | TEXT UNIQUE | e.g., "claude-code", "kiro", "aider" |
| display_name | TEXT | |
| command_json | TEXT | JSON array of command + args |
| resume_command_json | TEXT | JSON array, nullable |
| profile_json | TEXT | Full TerminalProfile serialized |
Populated from defaults + user `terminal_profiles.json` at startup.
### Sessions
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK | |
| project_id | FK → projects | |
| harness_id | FK → harnesses | |
| nickname | TEXT | User-assigned name, nullable |
| tmux_session_name | TEXT UNIQUE | e.g., "hqt-42" |
| harness_session_id | TEXT | The harness's own UUID for resume |
| model | TEXT | Selected model, nullable |
| created_at | TIMESTAMP | |
| last_activity_at | TIMESTAMP | |
| archived | BOOLEAN | |
### MCP Servers
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK | |
| name | TEXT UNIQUE | |
| transport | TEXT | "stdio" or "sse" or "http" |
| command | TEXT | For stdio |
| args_json | TEXT | JSON array |
| url | TEXT | For sse/http |
| env_json | TEXT | JSON dict |
| headers_json | TEXT | JSON dict |
### Project ↔ MCP Server bindings
| Column | Type | Notes |
|--------|------|-------|
| project_id | FK | |
| mcp_server_id | FK | |
| (composite PK) | | |
### Project ↔ Skill bindings
| Column | Type | Notes |
|--------|------|-------|
| project_id | FK | |
| skill_name | TEXT | References skill directory name |
| (composite PK) | | |
## Harness Config Injection
Before spawning a tmux session, `hqt` writes harness-specific config files so the harness picks up MCP servers and skills. This is the key abstraction layer.
```python
class HarnessConfigurator(Protocol):
def prepare(self, project: Project, session: Session, mcps: list[McpServer], skills: list[Skill]) -> SpawnConfig:
"""Write config files, return the command and env to spawn."""
...
@dataclass
class SpawnConfig:
command: list[str]
env: dict[str, str]
cwd: Path
cleanup: Callable[[], None] | None # remove temp config after session ends
```
### Per-harness injection strategies
| Harness | MCP Config | Skills/System Prompt |
|---------|-----------|---------------------|
| Claude Code | `~/.claude/settings.json``mcpServers` section, or project `.mcp.json` | `CLAUDE.md` in project root (append skill content) |
| Kiro CLI | Not applicable in TUI mode (Kiro manages own MCP) | Custom instructions file |
| Aider | `--read` flag for context files | Prepend to first message or `.aider.conf.yml` |
| Codex | `.codex/config.json` or env vars | `AGENTS.md` in project root |
| Generic | Env vars if supported | Prepend to first `send_text` |
## TUI Layout (Textual)
### Main Screen — Session Navigator
```
┌─────────────────────────────────────────────────────────┐
│ HQ Terminal [?] Help [q] │
├───────────────────┬─────────────────────────────────────┤
│ PROJECTS │ SESSIONS (project-a) │
│ │ │
│ > project-a │ ● claude-code "feature-xyz" alive │
│ project-b │ ○ kiro "refactor" dead │
│ project-c │ ● aider "bugfix-123" alive │
│ │ │
│ [a] Add project │ [n] New session [d] Delete │
│ │ [Enter] Attach [r] Rename │
└───────────────────┴─────────────────────────────────────┘
│ Status: 3 projects, 5 sessions (3 alive) │
└─────────────────────────────────────────────────────────┘
```
### New Session Dialog
```
┌─── New Session ────────────────────────┐
│ Project: project-a │
│ Harness: [claude-code ▼] │
│ Name: [________________] │
│ Skills: [brainstorming, workflow] │
│ MCPs: [hq-meta, custom-api] │
│ │
│ [Create] [Cancel] │
└─────────────────────────────────────────┘
```
### Key Bindings (Navigator)
| Key | Action |
|-----|--------|
| `j/k` or `↑/↓` | Navigate lists |
| `Enter` | Attach to selected session (switch tmux client) |
| `n` | New session dialog |
| `a` | Add project |
| `d` | Delete/archive session |
| `r` | Rename session |
| `Tab` | Switch focus between project/session panels |
| `q` | Quit TUI (sessions stay alive in tmux) |
| `/` | Filter/search |
## tmux Integration
### Session Naming Convention
`hqt-{session_db_id}` — e.g., `hqt-42`. Deterministic, avoids collisions.
### Startup Behavior
When `hqt` launches:
1. Check if already inside tmux (`$TMUX` set)
- If yes: create a new window in the current tmux session for the TUI
- If no: create a new tmux session `hqt-main` and run the TUI inside it
2. TUI attaches and displays the navigator
### Switching to a Session
```python
async def attach_session(tmux_session_name: str):
# From within tmux, switch the client to the target session
await tmux_runner.switch_client(tmux_session_name)
```
### Switching Back to TUI
Option A: tmux keybinding — bind a key in tmux.conf to `switch-client -t hqt-main`
Option B: `hqt` installs a tmux hook or provides a helper script `hqt-back`
Recommended: Option A with auto-setup. On first run, `hqt` suggests adding to `~/.tmux.conf`:
```
bind o switch-client -t hqt-main
```
### Liveness Detection
To show alive/dead status for sessions:
```python
async def is_session_alive(tmux_session_name: str) -> bool:
return await tmux_runner.has_session(tmux_session_name)
```
Polled periodically (every 2-5s) or on TUI focus.
## Skills System
Identical to HQ's current system:
- Skills live in `~/.config/hq/skills/{name}/SKILL.md`
- Frontmatter provides `name` and `description`
- Content is the full skill text
- Skills are bound to projects (stored in DB)
- At session spawn, bound skills are injected per the harness configurator
## MCP Server Management
- Global MCP server definitions stored in DB
- Bound to projects via junction table
- At session spawn, the harness configurator writes bound MCPs into the harness-specific config format
- UI provides CRUD for MCP servers and project bindings
## Configuration
Config file at `~/.config/hqt/config.toml`:
```toml
[general]
# Path to tmux binary
tmux_path = "tmux"
# TUI session name
tui_session_name = "hqt-main"
[harnesses]
# Override harness paths
claude_path = "claude"
kiro_path = "kiro"
[shortcuts]
# tmux key to switch back to TUI
switch_back_key = "o"
```
## CLI Interface
```
hqt # Launch TUI (default)
hqt list # List projects and sessions (for scripting)
hqt attach <session> # Attach to a session by name/id
hqt new <project> <harness> # Create session non-interactively
hqt doctor # Check tmux, harness availability
```
## Sandboxing (Future — v2)
Design hook for bubblewrap isolation:
```python
@dataclass
class SandboxConfig:
enabled: bool = False
read_only_root: bool = True # --ro-bind / /
project_writable: bool = True # --bind project_dir project_dir
network: bool = True # allow network access
extra_binds: list[tuple[str, str]] = field(default_factory=list)
def wrap_command(cmd: list[str], project_dir: Path, sandbox: SandboxConfig) -> list[str]:
if not sandbox.enabled:
return cmd
bwrap = ["bwrap"]
if sandbox.read_only_root:
bwrap += ["--ro-bind", "/", "/"]
if sandbox.project_writable:
bwrap += ["--bind", str(project_dir), str(project_dir)]
bwrap += ["--dev", "/dev", "--proc", "/proc"]
if not sandbox.network:
bwrap += ["--unshare-net"]
for src, dst in sandbox.extra_binds:
bwrap += ["--bind", src, dst]
bwrap += ["--"]
return bwrap + cmd
```
## Package Structure
```
hqt/
├── pyproject.toml
├── src/
│ └── hqt/
│ ├── __init__.py
│ ├── __main__.py # python -m hqt
│ ├── cli.py # click/typer CLI entry points
│ ├── config.py # Settings from config.toml + env
│ ├── db/
│ │ ├── models.py # SQLAlchemy models
│ │ ├── session.py # Engine/session factory
│ │ └── migrate.py # Schema evolution
│ ├── tui/
│ │ ├── app.py # Textual App subclass
│ │ ├── screens/
│ │ │ ├── navigator.py # Main project/session navigator
│ │ │ └── new_session.py # New session modal
│ │ ├── widgets/
│ │ │ ├── project_list.py
│ │ │ ├── session_list.py
│ │ │ └── status_bar.py
│ │ └── styles.tcss # Textual CSS
│ ├── tmux/
│ │ ├── runner.py # TmuxRunner (from HQ)
│ │ └── manager.py # Session lifecycle (spawn, attach, kill)
│ ├── harnesses/
│ │ ├── registry.py # Available harnesses discovery
│ │ ├── profiles.py # TerminalProfile + loading
│ │ └── configurators/
│ │ ├── base.py # Protocol + SpawnConfig
│ │ ├── claude.py # Claude Code config injection
│ │ ├── kiro.py # Kiro config injection
│ │ ├── aider.py # Aider config injection
│ │ └── generic.py # Fallback (env vars only)
│ ├── skills/
│ │ ├── registry.py # From HQ
│ │ └── injection.py # From HQ
│ ├── mcp/
│ │ ├── service.py # CRUD
│ │ └── serializers.py # Per-harness MCP config writers
│ └── projects/
│ └── service.py # Project CRUD + git status
└── tests/
```
## Tech Stack
- Python ≥ 3.12
- Textual (TUI framework)
- SQLAlchemy (database)
- SQLite (storage)
- Click or Typer (CLI)
- uv (package management)
- pytest (testing)
## Non-Goals (explicitly out of scope)
- Streaming harness responses back to the TUI (user interacts directly in tmux)
- Turn tracking / event sourcing
- Token usage tracking (harness-native UIs show this)
- Web interface
- Remote/multiplexer access beyond tmux's native capabilities
- Real-time output mirroring in the navigator (user attaches to see output)
## Decisions (from brainstorm)
1. **Session resume after reboot** — Yes, fully supported. Each session stores the harness's own session UUID so resume can reconnect to the same conversation. If a tmux session is dead but the DB record exists, the TUI offers to restart it using the harness's `resume_command` + stored UUID.
2. **tmux topology** — Flat: one tmux session per harness session. Naming: `hqt-{db_id}` (human-readable prefix, unique suffix).
3. **Skills directory** — Copy the skills system from HQ; use `~/.config/hqt/skills/` as its own directory (independent from HQ).
4. **Switch-back mechanism**`hqt` sets up a tmux keybinding on first run (default: `prefix o``switch-client -t hqt-main`). Configurable.
5. **Skill injection timing** — Skills are injected at spawn time. Mid-session skill changes require session restart for v1.
## Harness Abstraction Layer (Critical Design)
This is the most important abstraction in the system. Every harness has its own way of handling session identity, config injection, model selection, and MCP setup. The abstraction must normalize these.
### What the abstraction manages per-harness
| Concern | Description |
|---------|-------------|
| **Session UUID** | Each harness has its own persistent session/conversation ID. Must be stored in DB and passed on start/resume. |
| **Spawn command** | How to start a fresh session (with UUID, model, cwd) |
| **Resume command** | How to reconnect to an existing conversation after tmux death |
| **Model selection** | How to specify which model to use (CLI flag, env var, config file) |
| **MCP injection** | How to configure MCP servers for this harness (config file path + format) |
| **Skill injection** | How to provide system prompt / skill content (CLAUDE.md, AGENTS.md, etc.) |
| **Ready detection** | How to know the harness TUI is ready to accept input (regex on screen) |
| **Cancel mechanism** | How to interrupt a running turn (Ctrl-C, etc.) |
### Protocol definition
```python
from dataclasses import dataclass, field
from pathlib import Path
from typing import Protocol
@dataclass
class SpawnConfig:
"""Everything needed to launch a harness session in tmux."""
command: list[str] # Full command to execute
env: dict[str, str] # Extra environment variables
cwd: Path # Working directory
tmux_width: int = 120
tmux_height: int = 40
@dataclass
class HarnessIdentity:
"""Persistent identity for a harness session."""
harness_session_id: str # The harness's own UUID/session identifier
metadata: dict = field(default_factory=dict) # Harness-specific extras
class HarnessConfigurator(Protocol):
"""Abstraction for harness-specific configuration and lifecycle."""
name: str # e.g., "claude-code"
display_name: str # e.g., "Claude Code"
def generate_session_id(self, hqt_session_id: int) -> str:
"""Generate a deterministic harness session UUID for a new session."""
...
def build_spawn_config(
self,
project_path: Path,
harness_session_id: str,
model: str | None = None,
mcps: list[McpServerConfig] | None = None,
skills: list[Skill] | None = None,
) -> SpawnConfig:
"""Prepare config files and return the spawn command."""
...
def build_resume_config(
self,
project_path: Path,
harness_session_id: str,
model: str | None = None,
mcps: list[McpServerConfig] | None = None,
skills: list[Skill] | None = None,
) -> SpawnConfig:
"""Prepare config for resuming an existing conversation."""
...
@property
def supported_models(self) -> list[str]:
"""Models this harness supports (for UI dropdown)."""
...
@property
def ready_regex(self) -> str | None:
"""Regex to detect when the harness TUI is ready for input."""
...
@property
def busy_regex(self) -> str | None:
"""Regex to detect when the harness is actively processing."""
...
```
### Per-harness implementation notes
#### Claude Code
- **UUID**: `--session-id {uuid}` on start, `--resume {uuid}` on resume
- **UUID generation**: `uuid5(namespace, f"hqt-{session_id}")` — deterministic from DB id
- **Model**: `--model {model}` CLI flag
- **MCP**: Write to project `.mcp.json` or `~/.claude/settings.json`
- **Skills**: Append to `CLAUDE.md` in project root (or create it)
- **Ready regex**: `` (the input prompt marker)
- **Busy regex**: `esc to interrupt`
#### Kiro CLI
- **UUID**: Kiro manages its own sessions under `~/.kiro/sessions/cli/`
- **Model**: Config file or CLI flag (TBD based on Kiro TUI mode)
- **MCP**: Kiro manages its own MCP in TUI mode
- **Skills**: Custom instructions mechanism
- **Ready regex**: TBD
#### Aider
- **UUID**: Aider uses git repo as implicit session; `--restore-chat-history` resumes
- **Model**: `--model {model}` CLI flag
- **MCP**: Not applicable (Aider doesn't support MCP)
- **Skills**: `--read {file}` for context files, or prepend to first message
- **Ready regex**: `>` or `aider>` prompt
#### Codex CLI
- **UUID**: Codex uses thread IDs internally
- **Model**: `--model {model}` CLI flag
- **MCP**: `.codex/config.json` or `--config` flag
- **Skills**: `AGENTS.md` in project root
- **Ready regex**: TBD
### Session state management
The DB stores the harness session UUID. tmux liveness is the source of truth for "is it running":
```python
@dataclass
class SessionState:
db_exists: bool # Row in sessions table
tmux_alive: bool # tmux has-session returns 0
harness_session_id: str # Stored UUID for resume
@property
def status(self) -> str:
if not self.db_exists:
return "unknown"
if self.tmux_alive:
return "alive"
return "dead" # Can be resumed
```
The TUI polls tmux liveness periodically (every 3s) and updates the display. When a session transitions from alive → dead unexpectedly, the TUI can notify the user and offer restart.
+12
View File
@@ -0,0 +1,12 @@
# Lessons
## 2026-06-09: tmux windows vs sessions
- **Symptom:** Enter/Shift+R on sessions did nothing; `tmux list-sessions` only showed main session
- **Cause:** Harness sessions were separate tmux sessions. `switch-client` moves the entire client away. Sessions died silently with no error feedback.
- **Fix:** Redesigned to use windows within a single `hqt-main` session. `select-window` stays in context. `remain-on-exit` + `respawn-pane` handles dead processes cleanly.
- **Signal:** If "attach" ever silently fails again, check: does the tmux target actually exist? Is the error being swallowed?
## General
- tmux `remain-on-exit` must be set BEFORE the command can exit (race condition if set after `new-session`/`new-window`)
- `respawn-pane -k` restarts a dead pane in-place without destroying the window — preferred over kill+recreate
- Always check and propagate tmux command return codes; silent failures are the worst debugging experience