Skip to content

STRUCTURE

Mike Crowe edited this page Jul 5, 2026 · 4 revisions

harnessed — Directory Structure

Generated codebase map. The authoritative vocabulary (agent/recipe/service/stack/catalog) is in ARCHITECTURE.md at the repo root; the data-flow and key abstractions are in docs/codebase/ARCHITECTURE.md. This document covers where things live, naming conventions, and where to add new code.


Top-level layout

harnessed/
├── src/harnessed/          # Python application — CLI + assembly + launch logic
├── catalog/                # Authored content: agents, recipes, services, stacks
├── tests/                  # pytest (unit + podman-gated integration)
├── schemas/                # JSON Schema files for catalog YAML validation
├── docs/                   # GitHub wiki clone (git -C docs pull to update)
│   ├── codebase/           # Generated codebase maps (regenerate with /map-codebase)
│   └── guides/             # Recipe/service authoring guides
├── pyproject.toml          # Python project: name=harnessed, entry points, deps
├── mise.toml               # Runtime version pinning (Python, Node)
├── ARCHITECTURE.md         # Vocabulary + build model — read first
├── CONTRIBUTING.md         # Dev setup + how to add catalog entries
├── AGENTS.md               # Operational notes for AI assistants
└── CLAUDE.md               # Project instructions for AI assistants

Python source: src/harnessed/

src/harnessed/
├── __init__.py
├── launcher.py         # `harnessed` CLI — Typer app; build/launch/init/test commands
├── cli.py              # `harnessed-tools` CLI — argparse; assemble/scan/persist commands
├── assemble.py         # Assembly orchestrator — reads catalog, calls emit
├── emit.py             # File emitter — writes profile artifacts (EMIT ONLY, no podman)
├── schema.py           # Typed dataclasses + catalog YAML loaders + validation
├── paths.py            # Single source of truth for all path computation
├── synclinks.py        # Skills/commands fan-out with collision detection
├── persist.py          # Global persist allowlist + hard-deny guard
├── persist_gc.py       # Persist directory lifecycle (list + prune)
├── capability.py       # Capability test runner — launches headless, probes instance
├── report.py           # Capability test reporter — Rich table or JSON output
└── scan.py             # Supply-chain scan runners (osv-scanner, pip-audit, snyk)

Entry points (declared in pyproject.toml):

  • harnessedharnessed.launcher:main (Typer app)
  • harnessed-toolsharnessed.cli:main (argparse)

Import graph (top-down, no cycles):

launcher.py
  └─ assemble.py ─── emit.py ─── paths.py
  │                 └─ schema.py
  │
  └─ schema.py ────── paths.py
  └─ paths.py
  └─ persist.py ───── paths.py, schema.py
  └─ synclinks.py ─── schema.py

cli.py
  └─ assemble.py (same subgraph as above)
  └─ capability.py ── schema.py, paths.py
  └─ report.py
  └─ scan.py
  └─ persist_gc.py ── paths.py

Catalog: catalog/

catalog/
├── agents/
│   ├── claude/
│   │   ├── agent.yaml          # harness, image, dockerfile
│   │   └── (Dockerfile lives at catalog/base/Dockerfile.harnessed-claude)
│   └── omp/
│       └── agent.yaml
├── base/
│   ├── Dockerfile.harnessed-base       # shared base: node, mise, uvx, hatago, scanner
│   ├── Dockerfile.harnessed-claude     # claude CLI installer (FROM harnessed-base)
│   ├── Dockerfile.harnessed-omp        # omp installer (FROM harnessed-base)
│   ├── Dockerfile.harnessed-opencode   # opencode installer
│   ├── Dockerfile.harnessed-gemini     # gemini CLI installer
│   ├── Dockerfile.harnessed-antigravity
│   ├── Dockerfile.harnessed-codex
│   ├── egress-firewall.sh              # mounted into containers at launch
│   └── harnessed-scan                  # in-image supply-chain scan script
├── recipes/
│   └── <name>/
│       ├── recipe.yaml         # required
│       ├── Dockerfile          # optional — no FROM, no ARG HARNESS
│       ├── skills/
│       │   └── <skill-name>/   # leaf dir fanned into .claude/skills/
│       ├── commands/
│       │   └── <cmd-name>/     # leaf dir fanned into .claude/commands/
│       └── rules/
│           └── <rule-name>/    # leaf dir fanned into .claude/rules/
├── recipes.local -> ~/.config/harnessed/catalog/recipes  (symlink, created by `harnessed init`)
├── services/
│   └── <name>/
│       ├── service.yaml        # port, image, run command
│       └── Dockerfile
├── services.local -> ~/.config/harnessed/catalog/services
├── stacks/
│   └── <agent>_<recipe>[_<recipe>…]/
│       └── stack.yaml          # harness, recipes[], services[]
├── stacks.local -> ~/.config/harnessed/catalog/stacks
├── agents.local -> ~/.config/harnessed/catalog/agents
└── (agents.local, recipes.local, etc. symlinks created by `harnessed init`)

Naming conventions

Entity Convention Example
Stack <agent>_<recipe>[_<recipe>…] — underscores between fields, hyphens within a name claude_gstack_ping_time
Recipe single kebab-case name codebase-memory-mcp
Agent single lowercase name matching the harness claude, omp
Service single kebab-case name ping-service
Skill dir leaf dir name under skills/ time-helper
Command dir leaf dir name under commands/ bd-cmd
Derived image harnessed-<stack>:latest harnessed-claude-gstack-ping:latest
Instance name harnessed-<stack>-<sha1[:8]> harnessed-claude-gstack-ping-ab12cd34

Schemas: schemas/

schemas/
├── recipe.schema.json   # JSON Schema for recipe.yaml
├── stack.schema.json    # JSON Schema for stack.yaml
└── agent.schema.json    # JSON Schema for agent.yaml

YAML files in the catalog reference these via the yaml-language-server directive:

# yaml-language-server: $schema=../../../schemas/recipe.schema.json

Tests: tests/

tests/
├── fixtures/                       # fixture catalog trees (minimal stacks/recipes for unit tests)
├── __init__.py
├── test_schema.py                  # recipe/stack YAML parsing and validation
├── test_emit.py                    # artifact emission (mcp.json, hatago.config, settings.json, Dockerfile)
├── test_paths.py                   # path resolution (profile dirs, persist dirs, instance names)
├── test_synclinks.py               # skill/command fan-out and collision detection
├── test_persist_mounts.py          # persist bind-mount construction
├── test_persist_allowlist.py       # global persist hard-deny + allowlist guard
├── test_persist_gc.py              # persist list/prune lifecycle
├── test_launcher_install.py        # launcher image-build helpers
├── test_launcher_init.py           # one-time init container dispatch
├── test_launch_secrets.py          # varlock secret resolution
├── test_claude_config_seed.py      # per-instance .claude.json stub
├── test_omp_auth_seed.py           # omp agent mount
├── test_service_refs.py            # service-reference URL resolution
├── test_scan.py                    # supply-chain scan runners
├── test_ensure_docs_wiki_clone.py  # docs/ wiki bootstrap
├── test_ensure_local_catalog_links.py  # catalog symlink setup
├── test_catalog_json_schemas.py    # every catalog YAML validates against its JSON Schema
└── test_recipes_integration.py     # LIVE: builds each stack; asserts declared capabilities in container
                                    # Gated: HARNESSED_PODMAN=1 uv run pytest tests/test_recipes_integration.py

Run tests:

uv run pytest -q                                     # fast unit tests — no containers
HARNESSED_PODMAN=1 uv run pytest tests/test_recipes_integration.py  # live integration

Generated profile: $XDG_DATA_HOME/harnessed/profiles/<stack>/

Profiles are not in the repo — they are written to ~/.local/share/harnessed/profiles/<stack>/. Overridable via $XDG_DATA_HOME. Structure after a successful build:

profiles/<stack>/
├── .mcp.json                       # single hatago entry (claude --mcp-config points here)
├── hatago.config.json              # full MCP server list for the hatago hub
├── settings.json                   # merged settings: installer-baked + harnessed's required grants
├── Dockerfile.harnessed-<stack>    # generated derived Dockerfile (for inspection/debugging)
├── scan-report.json                # supply-chain scan result (surface from the built image)
└── .claude/
    ├── skills/
    │   └── <skill-name>/           # fanned from recipe dirs + extracted from image layers
    ├── commands/
    │   └── <cmd-name>/
    ├── rules/
    │   └── <rule-name>/
    ├── agents/, hooks/             # extracted from image if baked
    └── (settings.json is at profile root, not here)

Persist storage: $XDG_DATA_HOME/harnessed/persist/

Per-recipe, per-project dirs for recipe state that must outlive container restarts:

persist/
└── <recipe-name>/
    └── <project-hash-8hex>/
        └── <entry-name>/         # bind-mounted rw into the container at $HOME/<name>

The project hash is sha1(project_path)[:8] for scope: workspace, or sha1(git_common_dir)[:8] for scope: project. Managed by persist_gc.py.


User overlay catalog: ~/.config/harnessed/catalog/

~/.config/harnessed/catalog/
├── agents/       # user-private agents (override repo agents by name)
├── recipes/      # user-private recipes
├── services/     # user-private services
└── stacks/       # user-private stacks

The .local symlinks under catalog/ (e.g. catalog/recipes.local) point here, so a repo checkout can reference user-overlay entries directly when developing locally.


Where to add new code

Task Location
New recipe catalog/recipes/<name>/recipe.yaml (+ optional Dockerfile, skills/, commands/, rules/)
New agent harness catalog/agents/<name>/agent.yaml + catalog/base/Dockerfile.harnessed-<name> + entry in launcher._HARNESS_ATTACH_CMD
New service catalog/services/<name>/service.yaml + Dockerfile
New stack (compose recipes) catalog/stacks/<agent>_<recipe>[…]/stack.yaml
New assembler-level validation schema.py (parse) + assemble.py (gate)
New emitted artifact emit.py function + call site in assemble.py
New launcher command launcher.py Typer @app.command()
New harnessed-tools subcommand cli.py argparse subparser + _run_* handler
New path computation paths.py (NEVER compute paths in launcher.py directly)
New persist logic persist.py (security gate) or persist_gc.py (lifecycle)
New capability probe capability.py + schema.expected_capabilities
New supply-chain scan scan.py
Tests tests/test_<module>.py mirroring the module name

Key constraints to preserve

  • EMIT ONLY in assemble.py and emit.py — no subprocess, no podman calls.
  • paths.py is the single source for all path computations — never derive profile dirs or instance names inline in launcher.py.
  • No harnesses: field in recipe YAML — recipes are harness-independent; per-harness behavior belongs inside the recipe's Dockerfile via ARG HARNESS / ${HARNESS}.
  • Pin every download in recipe Dockerfiles — validate_pin rejects :latest, @latest, --branch main/master at assemble time.
  • pnpm, not npm/npxvalidate_no_raw_npm enforces this at assemble time.
  • Streamable-HTTP MCP only — SSE is deprecated; type: http in .mcp.json.
  • User overlay winspaths.find_in_catalog searches user catalog before repo catalog.
  • Credentials never baked — secrets arrive at launch via varlock env-file or ro credential mounts; never --build-arg/ENV in Dockerfiles.

Clone this wiki locally