diff --git a/.claude/ICP.md b/.claude/ICP.md new file mode 100644 index 0000000..53a9dda --- /dev/null +++ b/.claude/ICP.md @@ -0,0 +1,75 @@ +# OpenHarness Ideal Customer Profile + +This document fixes the v1 ICP. It is written to be read by both humans +making editorial decisions about README/docs/blog, and the orchestrator +deciding whether a feature request fits the product. When a request +doesn't fit, point at this document and decline. + +## Primary persona + +A **single working developer** building one ambitious project who wants +an autonomous agent loop helping with that project on a laptop or a +small cloud VPS they own. They: + +- write code professionally (or are pursuing it seriously) +- run Docker locally and are comfortable on a terminal +- use Claude Code (or are willing to) as their default coding agent +- want their host machine to stay clean — toolchains live in a sandbox +- value owning the harness over renting a SaaS + +This is **one developer, one project, one harness** — agent-tended +over time. The harness lives in the project repo and ships with it. + +## Jobs to be done + +1. **Stop polluting my laptop.** Every new agent CLI ships its own + Node version, Python version, system tool list. The sandbox absorbs + that — host stays Docker + git + an editor. +2. **Give my coding agent durable context.** Identity, memory, and + schedule files live in the repo, version-controlled, so the agent + picks up where it left off across sessions and machines. +3. **Run scheduled work without standing up infrastructure.** A small + cron runtime inside the sandbox fires recurring tasks (cleanup, + wiki lint, PR babysit) — no Kubernetes, no managed scheduler, no + third-party service. + +## Top three needs + +1. **One-command provisioning.** `docker compose up -d --build` on a + fresh laptop produces a working sandbox in under ten minutes. +2. **A clean upgrade path.** The harness is a folder of plain files + (Dockerfile, compose, scripts, markdown) — version it, fork it, + diff it, merge it. No hidden state. +3. **Extension via packs, not core changes.** Add Pi/Mom Slack, a + different default agent, custom crons — install a pack, leave the + core harness alone. + +## Anti-ICP — who this is NOT for + +| Not for | Why | +|---|---| +| **Multi-tenant SaaS operators** | OpenHarness is a per-developer harness, not a hosted multi-tenant agent runtime. No quota, no billing, no tenant isolation beyond the host's user/UID model. | +| **Non-developers** | Setting up the sandbox requires Docker, git, a terminal, and willingness to read a Dockerfile when something breaks. There is no GUI, no managed dashboard, no "just sign up" path. | +| **Production agent operators** | The sandbox is a development substrate. It has no SLA, no observability beyond tmux logs, no multi-region replication. Don't run customer-facing agents on it. | +| **Comparison-shopping tinkerers** | OpenHarness is opinionated: Claude Code default, Docker substrate, single project per harness, packs for the rest. If the appeal is "let me race five CLIs side-by-side and pick a winner," that workflow is not the v1 product surface — pin v0 (April 2026) for it. | + +## How to apply this + +When evaluating a feature request, doc rewrite, or marketing copy: + +- Does it serve **one developer building one project**? Keep. +- Does it serve **multi-tenant SaaS operators** or **non-developers**? + Decline; point at this file. +- Does it require **multi-agent comparison framing** to make sense? + Belongs in a pack (e.g., `@ryaneggz/mifune`) or a v0 archived blog + post — not in the v1 surface. +- Is it **production observability / SRE work**? Out of scope; treat + the sandbox as a dev environment, not a runtime platform. + +## Versioning + +This file is the v1 ICP statement. v0 framing (multi-agent +comparison, "run Claude/Codex/Gemini side-by-side") is preserved in +`blog/archive/` for history and explicitly rejected here. Future ICP +shifts get a new version section above this one — never edit the +existing statement; append. diff --git a/CHANGELOG.md b/CHANGELOG.md index a24e20e..bfdc2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ Update policy and release automation live in [`.claude/rules/git.md`](.claude/ru ## [Unreleased] ### Removed +- **BREAKING**: multi-agent comparison narrative archived. Deleted the still-draft `blog/2026-04-29-worktree-per-agent.md`; moved `blog/2026-04-28-byoh.md` to `blog/archive/2026-04-28-byoh.md` with `archived: true` frontmatter and a top-of-post note that v1 framing differs. Stripped the residual "side-by-side" wording from `docs/agents/overview.md`. Per SPEC v0.7 §"oh CLI" + §"Documentation source"; the v1 ICP statement at `.claude/ICP.md` now explicitly rejects multi-agent comparison-shopping framing as out-of-scope. ([#210](https://github.com/ryaneggz/open-harness/issues/210)) - **BREAKING**: `@openharness/sandbox` CLI removed; `docker compose` is the canonical substrate. Deleted the `packages/sandbox/` workspace package (CLI binary + harness/onboard/lib/tools source + tests), the entry from `pnpm-workspace.yaml`, the `link:cli` script from root `package.json`, the now-stale `.github/workflows/ci.yml` (sandbox-only CI), and the duplicate `.github/workflows/build.yml` (which had been pushing a drifted `ghcr.io/ryaneggz/open-harness` image alongside the canonical `ghcr.io/ryaneggz/openharness` produced by `release.yml`). The CLI build/symlink block in `.devcontainer/entrypoint.sh` that wired `/usr/local/bin/openharness` and `/usr/local/bin/oh` is gone. `release.yml` is now the sole image producer. Per SPEC v0.7 §"oh CLI". ([#210](https://github.com/ryaneggz/open-harness/issues/210)) - **BREAKING**: heartbeat daemon removed. Deleted `packages/sandbox/src/lib/heartbeat/` (~1,752 LOC), `packages/sandbox/src/cli/heartbeat-daemon.ts`, `packages/sandbox/src/cli/actions/heartbeat.ts`, `packages/sandbox/src/tools/heartbeat.ts`, the `oh heartbeat {start,stop,status}` command group, the `heartbeat-daemon` package bin, the seven `heartbeat-*.test.ts` suites, and `.claude/skills/heartbeat/`. Heartbeat env vars (`HEARTBEAT_ACTIVE_START`/`_END`/`_AGENT`) stripped from `.devcontainer/docker-compose.yml`; the legacy daemon-startup blocks in `install/entrypoint.sh` and the `/usr/local/bin/heartbeat-daemon` symlink in `.devcontainer/entrypoint.sh` are gone; `install/banner.sh` no longer advertises `heartbeat-daemon status`. The croner runtime (`scripts/cron-runtime.ts`, US-002/US-003/US-004) is the canonical replacement per SPEC v0.7 §"Croner runtime". ([#210](https://github.com/ryaneggz/open-harness/issues/210)) - **BREAKING**: `@mariozechner/pi-coding-agent` is no longer a dependency. The pi agent CLI and Mom Slack bot have been extracted to a separate harness pack at [`ryaneggz/mifune`](https://github.com/ryaneggz/mifune) (npm: `@ryaneggz/mifune`). Install with `oh harness add @ryaneggz/mifune`. ([#208](https://github.com/ryaneggz/open-harness/pull/208)) ### Added +- v1 Ideal Customer Profile statement at `.claude/ICP.md`: primary persona (single working developer, one project, agent-tended), top three jobs-to-be-done, top three needs, and explicit anti-ICP (not multi-tenant SaaS, not non-developers, not production agent operators, not comparison-shopping tinkerers). ([#210](https://github.com/ryaneggz/open-harness/issues/210)) - Seeded the two SPEC-reserved crons: `crons/heartbeat.md` (hourly pulse, `0 * * * *`) and `crons/cleanup-tasks.md` (Sunday 23:00, `0 23 * * 0`). Bodies follow SPEC v0.7 §"Croner runtime" + §"Weekly cleanup cron"; substrate stays empty otherwise so users add their own. ([#210](https://github.com/ryaneggz/open-harness/issues/210)) - `.devcontainer/entrypoint.sh` now starts `scripts/cron-runtime.ts` in a `system-cron` tmux session on container boot, replacing the legacy `heartbeat-daemon` watchdog block. Idempotent (`tmux has-session` guard); restart policy is per-container-start per SPEC v0.7 §"Croner runtime". ([#210](https://github.com/ryaneggz/open-harness/issues/210)) - `scripts/cron-runtime.ts` — minimal croner-backed runtime (≤120 LOC) that reads `crons/*.md` frontmatter, schedules per-cron tasks, runs the body as a prompt to `claude -p` (or `$CRON_AGENT_BIN`), logs to `crons/cron-runtime.log`, and uses a PID-lock at `crons/.pid` to prevent double-firing. Replaces the 1,900-LOC heartbeat daemon per SPEC v0.7. ([#210](https://github.com/ryaneggz/open-harness/issues/210)) diff --git a/blog/2026-04-29-worktree-per-agent.md b/blog/2026-04-29-worktree-per-agent.md deleted file mode 100644 index 430912c..0000000 --- a/blog/2026-04-29-worktree-per-agent.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: "One Worktree Per Agent" -description: "We tried one container per agent. It cost $40/month per agent. Here is what we did instead — one container, many git worktrees, full isolation, one bill." -date: 2026-04-29 -authors: [ryan] -tags: [worktrees, agents, architecture] -draft: true ---- - -# One Worktree Per Agent - -We tried one container per agent. It cost $40 a month per agent. Here is what we did instead. - -## The $40 problem - -Spinning up a fresh container for every coding agent is the obvious first move. It is also the wrong one. A small VPS that can comfortably host a Claude Code session — Node, pnpm, Python, Docker-in-Docker, a checkout of the repo, and enough memory not to thrash — runs about $8 a month on a budget host. Multiply that by five agents and you are at $40 a month before you have shipped a single feature. Make it ten and you are paying $80 to keep ten copies of the same toolchain warm. - -The cost is the visible part of the problem. The hidden part is worse. Each container has its own `node_modules`, its own pnpm store, its own Docker image cache, its own git object database. A change to a shared dependency means rebuilding it five times. A new tool installed for one agent has to be reinstalled for the rest, or the agents diverge. There is no shared workspace, so when two agents need to look at the same generated artifact they each have to regenerate it. The agents are isolated, and that isolation has a real cost in money, in disk, and in the friction of keeping five identical setups identical. - -## The constraint - -What do agents actually need from each other? Two things, mostly: a separate branch and separate state. One agent should be able to commit to `feat/docs` while another commits to `feat/tests` without either one stepping on the other. One agent should be able to keep its memory, its current task, and its working files distinct from the next agent's. - -Notice what is **not** on that list. Agents do not need separate kernels. They do not need separate package managers. They do not need separate Docker daemons. They do not even need separate filesystems — they need separate **views** of a filesystem. - -Git already solves the "separate branch, separate working tree" problem cheaply. It is called `git worktree`. A worktree is a second working directory backed by the same `.git` object store as your main checkout. Each worktree is on a different branch, has its own staged index, its own untracked files. From the inside, an agent in a worktree looks like an agent that owns the whole repo. From the outside, it is just another directory pointing at shared objects. - -So the design fell out: one container, many worktrees, one agent per worktree. The container handles the toolchain and the kernel and the Docker socket. The worktree handles the branch and the working state. An agent gets full isolation where isolation matters and shares everything where sharing is free. - -## The architecture - -```mermaid -graph TD - Host[Host machine] --> Container[oh sandbox container
shared toolchain + docker socket] - Container --> WT1[.worktrees/feat-docs
AGENTS.md / SOUL.md / MEMORY.md] - Container --> WT2[.worktrees/feat-tests
AGENTS.md / SOUL.md / MEMORY.md] - Container --> WT3[.worktrees/review-all
AGENTS.md / SOUL.md / MEMORY.md] - Container --> HB[heartbeat scheduler
cron-style ticks] - WT1 --> A1[Claude Code
SOUL: writer
branch: feat/docs] - WT2 --> A2[Codex
SOUL: tester
branch: feat/tests] - WT3 --> A3[Pi
SOUL: reviewer
branch: review/all] - A1 --> Push1[git push origin feat/docs] - A2 --> Push2[git push origin feat/tests] - A3 --> Push3[git push origin review/all] - HB -.fires per worktree.-> A1 - HB -.fires per worktree.-> A2 - HB -.fires per worktree.-> A3 -``` - -One sandbox container at the top. Inside it, `oh worktree ` carves a new directory under `.worktrees/`. Each worktree gets its own `AGENTS.md` (operating procedures), `SOUL.md` (persona), `MEMORY.md` (running notes), and its own heartbeat config. The heartbeat scheduler runs once at the container level and fans out per worktree, so agent A's hourly check does not block agent B's nightly release. Every agent gets the same Docker socket — they can all build images, run containers, talk to the same registry — but no agent can see another agent's branch or working tree without crossing a directory boundary. - -The cost model collapses. One VPS, one toolchain, one pnpm store, one Docker image cache. The marginal cost of adding the sixth agent is whatever a new directory and a new branch costs, which is to say, nothing. - -## A real example - -Suppose you want a Claude Code agent writing documentation and a Codex agent writing tests, both on the same repo, both committing without stepping on each other. From the host you start the sandbox once: - -```bash -openharness sandbox up -``` - -Inside the sandbox you create two worktrees, one per agent: - -```bash -oh worktree feat/docs # cuts .worktrees/feat/docs from development -oh worktree feat/tests # cuts .worktrees/feat/tests from development -``` - -Each worktree comes with the workspace template — an `AGENTS.md` you edit to give that agent its operating rules, a `SOUL.md` for personality, a `MEMORY.md` it will write to as it works. You launch the agents in their own tmux sessions so they survive disconnects: - -```bash -tmux new-session -d -s agent-docs -c .worktrees/feat/docs 'claude' -tmux new-session -d -s agent-tests -c .worktrees/feat/tests 'codex' -``` - -Two agents, two branches, one container. Claude opens files under `.worktrees/feat/docs/` and commits to `feat/docs`. Codex does the same under `feat/tests`. They never see each other's working files. When they push, two separate PRs land on GitHub. You can attach to either tmux session at any time, watch the agent work, detach, and walk away. You could run five Claudes and five Codexes the same way — ten worktrees, ten branches, one bill. The constraint is your CPU, not your billing page. - -Heartbeats per worktree fire independently. Drop a `heartbeats/nightly-release.md` into the docs worktree and only the docs agent runs it. The tests worktree has its own `heartbeats/` directory and its own schedule. No global cron file to coordinate. - -## The honest tradeoffs - -What worktree-per-agent does **not** solve: concurrent writes to non-git state. Two agents installing different versions of the same npm package into a shared `node_modules` will fight. We handle that with per-worktree `node_modules` (a symlink dance, or just a plain `pnpm install` per worktree) — the disk cost is trivial because the pnpm store is shared, and most installs are hardlinks. If your toolchain has its own global cache (Cargo, Go modules, Maven), the same logic applies: shared store, per-worktree resolution. - -The other thing it does not solve is genuinely incompatible toolchains. If one agent needs Node 18 and another needs Node 22, you are back to multiple containers. We have not hit that yet — most coding agents are happy on the same Node — but it is the obvious cliff. - -## Try it - -The whole pattern lives in [open-harness](https://github.com/ryaneggz/open-harness). Clone it, run `openharness sandbox up`, run `oh worktree my-first-agent`, and you have your first isolated agent workspace in under a minute. The full walkthrough is in the [docs](/docs), and the companion piece on the wider design — [Bring Your Own Harness](./byoh.md) — dropped Tuesday and explains why the harness itself is a project worth owning, not a SaaS to rent. - -If you have been hesitating because you assumed multi-agent setups were expensive, they are not. They are one container and a few `git worktree add` commands away. diff --git a/blog/2026-04-28-byoh.md b/blog/archive/2026-04-28-byoh.md similarity index 94% rename from blog/2026-04-28-byoh.md rename to blog/archive/2026-04-28-byoh.md index 09e39b5..ed28ddc 100644 --- a/blog/2026-04-28-byoh.md +++ b/blog/archive/2026-04-28-byoh.md @@ -4,8 +4,11 @@ description: "My laptop had 14 Node versions, 3 Pythons, and a Rust toolchain I date: 2026-04-28 authors: [ryan] tags: [agents, docker] +archived: true --- +> **Archived.** This post reflects the v0 multi-agent framing. v1 OpenHarness is single-project: one repo, one sandbox, one default agent (Claude Code); multi-agent setups live in harness packs like [`@ryaneggz/mifune`](https://github.com/ryaneggz/mifune). Kept for history. + # BYOH — stop installing agent CLIs on your laptop I ran `ls ~/.nvm/versions/node` last week and counted **14 Node versions**. Three Pythons in `pyenv`. A Rust toolchain I'm pretty sure I installed for one Tauri experiment in 2023. A `~/.local/share/pnpm/global` directory that npm wouldn't admit existed. My laptop was a museum of dependency archaeology. diff --git a/docs/agents/overview.md b/docs/agents/overview.md index fc62491..35e047a 100644 --- a/docs/agents/overview.md +++ b/docs/agents/overview.md @@ -5,7 +5,7 @@ title: "Agents Overview" # Agents Overview -Open Harness ships with Claude Code and Codex preinstalled in the sandbox image. Additional agents (Gemini CLI, Pi Agent + Mom Slack bot) are available as opt-in installs or via [harness packs](../guide/bring-your-own-harness.md) like [`@ryaneggz/mifune`](https://github.com/ryaneggz/mifune). All agents can run side-by-side in the same sandbox, each in its own git worktree, with its own identity and its own cron schedule. +Open Harness ships with Claude Code preinstalled in the sandbox image as the default agent. Additional agents (Codex, Gemini CLI, Pi Agent + Mom Slack bot) are available via [harness packs](../guide/bring-your-own-harness.md) like [`@ryaneggz/mifune`](https://github.com/ryaneggz/mifune). Packs add their agents to the same sandbox, each in its own git worktree, with its own identity and its own cron schedule. ## Why run multiple agents at once? diff --git a/tasks/openharness-v07-convergence/prd.json b/tasks/openharness-v07-convergence/prd.json index ff83976..c7e87ec 100644 --- a/tasks/openharness-v07-convergence/prd.json +++ b/tasks/openharness-v07-convergence/prd.json @@ -194,8 +194,8 @@ "Typecheck passes" ], "priority": 11, - "passes": false, - "notes": "" + "passes": true, + "notes": "Completed iteration 11 on 2026-05-02; deleted blog/2026-04-29-worktree-per-agent.md, moved blog/2026-04-28-byoh.md to blog/archive/ with archived:true + v1-framing note, created .claude/ICP.md with primary persona + JTBD + top-3 needs + 4-row anti-ICP table; stripped residual 'side-by-side' wording from docs/agents/overview.md; grep gate passes (zero hits in README/docs/blog excluding archive)" }, { "id": "US-012", diff --git a/tasks/openharness-v07-convergence/progress.txt b/tasks/openharness-v07-convergence/progress.txt index 34cbf0b..2ee0fa0 100644 --- a/tasks/openharness-v07-convergence/progress.txt +++ b/tasks/openharness-v07-convergence/progress.txt @@ -233,3 +233,24 @@ Rewrote CLAUDE.md (the orchestrator instruction file) for docker-compose-only fl - **The `oh harness add @ryaneggz/mifune` instruction in CLAUDE.md was the LAST place that command appeared** in the orchestrator-instruction file. With `packages/sandbox/` gone the command no longer exists; per US-016 the v1 install path is `git clone` (the v0.8 issue captures the full pack-contract redesign). I rewrote the line to "install a harness pack by `git clone`-ing it ... follow its README" rather than naming a specific command — leaves room for v0.8 to land an actual install mechanism without re-editing CLAUDE.md. --- + +## US-011 — 2026-05-02 15:02 UTC + +**Title**: Archive multi-agent blogs and write anti-ICP +**Files changed**: blog/2026-04-29-worktree-per-agent.md (deleted), blog/2026-04-28-byoh.md → blog/archive/2026-04-28-byoh.md (moved + archived:true frontmatter + v1-framing top note), .claude/ICP.md (new), docs/agents/overview.md (drop side-by-side wording), CHANGELOG.md (Unreleased Removed + Added entries), tasks/openharness-v07-convergence/prd.json (passes:true) +**Commit**: +**Result**: PASS + +### What I did + +Killed the v0 multi-agent comparison-shopping narrative per Critic A's demand and locked in the v1 ICP. Deleted the still-draft worktree-per-agent post (cost was zero — never published). Moved the BYOH post to blog/archive/ with archived:true frontmatter and a one-line top-of-post note that the v1 framing differs (single-project, Docker, Claude Code default — multi-agent setups via @ryaneggz/mifune pack). Wrote .claude/ICP.md with primary persona (single working developer, one project, agent-tended over time), three JTBDs (stop polluting laptop, durable agent context, scheduled work without infra), top-3 needs (one-command provision, clean upgrade path, extension via packs), and an explicit 4-row anti-ICP table (not multi-tenant SaaS / not non-developers / not production agent operators / not comparison-shopping tinkerers) with a "how to apply" decision rubric. Stripped the residual "All agents can run side-by-side" wording from docs/agents/overview.md (the only side-by-side hit outside blog/) — replaced with single-default-Claude framing pointing at packs. Verified `grep -ri 'side-by-side|comparison-shop|14 Node versions' README.md docs/ blog/ | grep -v archive/` returns zero hits. CHANGELOG `[Unreleased] ### Removed` entry references #210 + BREAKING (multi-agent narrative), and `### Added` entry documents the new ICP.md. Lint clean; 9 cron-runtime tests pass. + +### Learnings for future iterations + +- **Move-then-edit gotcha for staged files.** `git mv` stages the rename; subsequent edits to the moved file modify the worktree but the staged blob remains the pre-edit content. Re-run `git add ` after the in-archive edits land or the commit will go in with the unedited byoh body. The `git status -s` output `RM` (renamed-modified) is the giveaway — `R` means stage has the rename, `M` means worktree has further changes. +- **Docusaurus blog plugin scans `path: '../../blog'` recursively** (per `apps/docs/docusaurus.config.ts:90`), so `blog/archive/*.md` files would still be served on the public blog without an explicit exclude. The AC didn't require gating the archive from public render — `archived: true` is a custom (non-standard) frontmatter key, so docusaurus ignores it. If a future story needs the archive *unrendered*, set `exclude: ['archive/**']` on the blog plugin or move `blog/archive/` outside `blog/` entirely. For now, archived stays public-but-marked. +- **The grep gate's `| grep -v archive/` pipe is what lets the archived BYOH post keep its "14 Node versions" hook intact.** Future cleanup stories that touch the archive directory should preserve that pipe in their grep checks; otherwise they'll false-positive on the archived-by-design content. +- **ICP.md placement is `.claude/ICP.md`** (alongside `specs/`, `rules/`, `skills/`, `agents/`), NOT at repo root. The orchestrator instruction surface is the `.claude/` dir; the ICP belongs there as the editorial-decision substrate (humans read it; the orchestrator references it when declining out-of-scope feature requests). +- **`docs/agents/overview.md` still references `oh harness add @ryaneggz/mifune`** at line 41 (in the "Installation" section). Out of scope for US-011's grep gate (the gate is `side-by-side|comparison-shop|14 Node versions`), but US-016's pack-contract redesign or a future cleanup pass should fix it. Same logic for the file's "running agents in parallel" tmux example — it's still per-agent multi-tmux, which v1 framing would reduce to a single `claude` invocation. Leave for editorial debt list. + +---