# Installation

Meerkat TaskPanel ships as **a local server**, **the board, director, chat console and five
role skills**, and **one plugin root** — the repository itself, which every harness reads in
place:

- **`meerkat`** (`skills/meerkat/`) — the board. Installed for every selected agent kind.
- **`meerkat-director`** (`skills/meerkat-director/`) — the decision hub and the
  supervisor's operator skill: assignment, worker questions and E1–E5 escalation. Installed
  on the **primary kind** (the explicit `--supervisor=enable` kind when one is named, else
  the first `claude|codex|pi` in `--target` order; `all` → claude).
  OpenClaw never gets it, because the openclaw-team framework already runs an equivalent
  poll / patrol (see [`scheduling.md`](../scheduling.md#openclaw-no-loop-units)). The
  supervisor finds the skill wherever it is installed at runtime; no config records its path.
- **`meerkat-chat`** (`skills/meerkat-chat/`) — the human's live console: progress, awaiting
  questions, pending acceptance, `loop doctor` and abnormal-card triage; records the human's
  own words with `--as-human`. Installed beside the director on the primary kind.
- **`jobs`, `linus`, `turing`, `reviewer`, `worker`** (`skills/<role>/`) — the five
  standalone role skills. Each is a complete role: `SKILL.md`, `role.json` and `references/`.
  A fresh install puts them on the primary kind; on an upgrade each role is refreshed on the
  kind(s) that already carry it and are in `--target`. Installation location is the platform
  the role runs on.
- **`setup-meerkat`** (`skills/setup-meerkat/`) — the user-invoked `/setup-meerkat`
  command: it detects what is installed, repairs what is missing, offers the Matt Pocock
  issue-tracker integration, and moves role skills between platforms. The bash installer
  copies it **on the primary kind only**; the repository bundle and marketplace still carry
  it for the other agent kinds.

> **"Actor kind" and "agent".** An **actor kind** is the application that runs the agent — Claude
> Code, OpenClaw, Codex or Pi / Agent Skills: the values `--target` takes.
> An **agent** is the identity recorded on the board (`--agent jobs|linus|turing`,
> `install.sh --agent-name`). Kind answers *which application*, agent answers *who*. A
> **host** in this repository means the machine, never the application.

There are three layers, and it is worth keeping them straight:

1. **The skills** — `SKILL.md` + `references/` + a wrapper, copied (or, for the board
   skill, symlinked) into the agent's skill directory by `install.sh`. This is
   deterministic, offline, and needs no agent tooling. The board skill is what makes the
   agent able to drive `taskctl`; the role skills carry each role's workflow, and the
   supervisor itself runs as `taskctl loop …`.
2. **The agent bundle** (Claude Code and Codex) — `install.sh --target claude|codex`
   deploys the rest of that agent's setup too, not just the skill: the agent's own settings
   (`~/.claude/settings.json` / `~/.codex/AGENTS.md`), slash commands (Claude)
   or the scheduling units (Codex), a `taskctl` shim that pins the agent identity, and the
   per-kind config at the **program root** (`<program root>/.env.<actor-kind>` — not under the
   agent directory). Writes
   are **merge-style and idempotent**: a repeat install **exits 0**, leaves every managed
   file and config key at its existing value, and prints a
   `left unchanged — use --force to overwrite` notice; `--force` is the *only* way to
   overwrite. See [Idempotence](#idempotence-re-running-and-force).
3. **The plugin/bundle** — the manifest that lets the agent's own plugin system discover
   and enable the skill. This step needs that agent's CLI, so `install.sh` does **not** run
   it; it prints the command and you run it once. The repository root *is* the board's
   plugin root (`.claude-plugin/plugin.json`), which is what lets every harness share the
   one `skills/` tree instead of receiving a copy. The marketplace has a single entry —
   `meerkat-taskpanel`, sourced at `./`. `meerkat-director`, `meerkat-chat`, the role skills
   and `setup-meerkat` are **not** separate marketplace plugins: the root bundle carries them
   from the same tree, and `install.sh` copies the director, chat console, setup command and
   roles into the **primary** agent kind's skill directory only.

> **Status: the engine has landed in full.** The skill is discoverable, the `taskctl`
> commands it describes are implemented, and the board backend (`src/server/`, HTTP + SSE)
> plus the `web/` frontend are in place. See `CLAUDE.md` for what is still planned (the
> Epic view and iteration management).

## Install methods

Two entry points drive **one** materializer, so an npm install and a source install produce
the same tree, the same launchers and the same config file.

### npm / npx (preferred)

```bash
npx meerkat-taskpanel                                    # server + taskctl + every agent-kind skill
npm install -g meerkat-taskpanel                         # or install the package globally …
meerkat-taskpanel install                                # … and run the installer whenever needed
npx meerkat-taskpanel install --target server            # server runtime + taskctl only
npx meerkat-taskpanel install --target claude,server,pi  # server + those two agents
npx meerkat-taskpanel install --no-server                # agent-kind skills only
```

With no subcommand, `install` is assumed. The wrapper ships inside the package, so `npx`
needs `meerkat-taskpanel` to be published (see [`publishing.md`](publishing.md)) — it fetches
nothing else. The published tarball carries the built `web/dist`, so the installed board
serves its UI with no build step.

### From a source checkout

```bash
bash install.sh --target all                # server + the skill for every agent (the default)
bash install.sh --target claude,server,pi   # server + those two agents
bash install.sh --target all --no-server    # the agent-kind skills, no server
bash install.sh --target server             # server runtime + taskctl only
bash install.sh --target claude             # one agent, no server
```

`all` is every agent **plus** the server; `--no-server` leaves the server out, and an
explicit comma list selects exactly the targets named. From a checkout, `web/dist` is
copied if present; if it is absent and `web/node_modules` exists the installer builds it;
otherwise it warns and installs an API-only server (it never runs `npm ci`).

### The server tree

The server target materializes a self-contained runtime at `<home>/.meerkat-taskpanel`
(`<home>` is `$HOME`, or `--prefix`):

```text
~/.meerkat-taskpanel/
├── src/**                # the whole program, migrations included
├── web/dist/**           # the built board (API-only if it could not be produced)
├── package.json
├── bin/taskd             # 0755 launcher: exec node <tree>/src/server/main.mjs
├── bin/taskctl           # 0755 launcher: exec node <tree>/src/cli/index.mjs
├── .env                  # 0600, seeded from the template; never overwritten
├── .env.<actor-kind>     # umask default; one per installed kind, policy not secrets
├── .data/                # 0700, the SQLite board; never deleted
└── logs/                 # 0700, the loop's supervisor.log for every actor kind
```

The tree is self-locating: `src/` resolves `.env`, `.env.<actor-kind>`, `.data` and `web/dist` from its own
position, so `bin/taskd` and `bin/taskctl` work from any directory and with no environment
variables. Add `~/.meerkat-taskpanel/bin` to `PATH`, or use `npm install -g`.

When the server is installed alongside agent-kind skills (the default, and every npm/npx
install), the agent shim and per-kind config (`MEERKAT_TASKPANEL_REPO`,
`MEERKAT_TASKPANEL_ENV_FILE`) point at this tree, never at the npx cache or the checkout,
and the `taskd` guard's unit runs the tree's `src/server/main.mjs` with
`TASKD_DATA_DIR=<tree>/.data`. An agent-only install (`--no-server`) is unchanged: it keeps
pointing at the checkout.

The installer is **idempotent**: a re-run exits 0 and leaves `.env` and `.data` byte-for-byte
alone; `src/` and the launchers are refreshed. `--force` additionally prunes stale files
under `src/` (it still never touches `.env` or `.data`); `--dry-run` writes nothing.

### Upgrading and removing

Upgrade by running the installer again (`--force` to refresh managed files). To remove,
delete the agent-kind skill directories listed below and the tree itself; **keep `.data/` if you
want the board**: `rm -rf ~/.meerkat-taskpanel` deletes the database with it.

**One loop log, at the config root.** The supervisor log used to sit under the agent
directory (`<agent-dir>/meerkat-taskpanel/supervisor.log`) and now lands at
`<program root>/logs/supervisor.log`, one file for every actor kind. The installer does not
move the old file: a re-run repoints the units, and the old
`<agent-dir>/meerkat-taskpanel/supervisor.log*` is dead weight you may delete by hand.

**Where the per-kind config lives.** `<program root>/.env.<actor-kind>` (for example
`.env.claude`), beside the board `.env`, not under the agent directory. A combined install
(`--target all`, or npm/npx) keeps it in the tree — `~/.meerkat-taskpanel/.env.claude` — and
removing the tree removes it. An agent-only install keeps it at the checkout/package root —
`<checkout>/.env.claude` — so remove that file by hand when uninstalling; a map you created
there (`<checkout>/projects.json`) goes with it. The same rule
applies to the installer's config: `MEERKAT_TASKPANEL_CONFIG_DIR` relocates the directory
(tests, containers, a read-only package root) **and the project map beside it**
(`<config root>/projects.json`). An **agent-only npm/npx install** keeps both inside the
package tree, which an upgrade replaces; prefer a combined install when the per-kind policy
or the map must survive upgrades.

**Migrating from `<agent-dir>/meerkat-taskpanel.env`.** The next `install.sh` copies the
legacy file once to the new location, appends any installer-owned keys it lacks, and leaves
the legacy file untouched — it is no longer read. An existing new file always wins (no merge);
`--force` resets the new file to the template instead of migrating. Switching install shape
(for example agent-only, then `--target all`) does not migrate the old program root's file;
copy `<old root>/.env.<kind>` to the new tree by hand.

**Migrating from `<agent-dir>/skills/meerkat-taskpanel/`.** The board skill installed under
that directory before it was renamed `meerkat`. The next `install.sh` writes the new
directory, moves the operator's `roles/` into it where the new directory does not already
hold it (the newer copy wins), and removes the old directory. A `projects.json` there is
machine-local and moves to the config root instead — merged into an existing map (the
config-root entry wins, source-only entries are added) and then removed; a file that is not
the documented shape is left in place with a warning. A `--link` install is left alone with a
line naming the manual move: writing through the symlink would land in the checkout it points
at. `--dry-run` only reports the migration.

## Quick start (agent-kind skills)

```bash
bash install.sh --target all --agent-name Worker    # everything; full bundle for claude+codex
bash install.sh --target claude --link --agent-name alice  # or one agent, symlinked, named alice
```

Then register the plugin/bundle for the agents you use (one command each — see
[Per-agent-kind steps](#per-agent-kind-steps)). `--agent-name` names the agent on the board and must
match the assignee the board uses for it; it defaults to `Worker`.

## Where things land

| Agent | `--target` | Board skill | Director + chat + setup | Role skills |
|---|---|---|---|---|
| Claude Code | `claude` | `~/.claude/skills/meerkat` | `~/.claude/skills/{meerkat-director,meerkat-chat,setup-meerkat}` **when primary** | `~/.claude/skills/<role>` when the role is assigned there |
| OpenClaw | `openclaw` | `~/.openclaw/skills/meerkat` | **none** — never a loop kind; see [OpenClaw](../scheduling.md#openclaw-no-loop-units) | **none** |
| Codex | `codex` | `~/.codex/skills/meerkat` | `~/.codex/skills/{meerkat-director,meerkat-chat,setup-meerkat}` **when primary** | `~/.codex/skills/<role>` when the role is assigned there |
| Pi / Agent Skills | `pi` | `~/.agents/skills/meerkat` | `~/.agents/skills/{meerkat-director,meerkat-chat,setup-meerkat}` **when primary** | `~/.agents/skills/<role>` when the role is assigned there |

**The primary kind** is the explicit `--supervisor=enable` actor kind when one is named,
else the first `claude|codex|pi` in normalized `--target` order — `--target all` → `claude`;
`--target codex,pi` → `codex`. It is the same primary `--supervisor=enable` acts on. The
director, the chat console, `setup-meerkat` and a fresh install's role skills go there, and
nowhere else. `--supervisor=disable`/`status` name no primary, so the first loop kind leads.

**Roles on an upgrade.** `install.sh` scans `<home>/{.claude,.codex,.agents}/skills/<role>/`
for each role. Every kind that already carries the role **and is in `--target`** is refreshed;
the primary kind is not given a second copy. A role whose only home is outside `--target` is
named on stderr and left alone — the write scope is exactly the declared targets. A role the
repository adds later has no installed home, so it defaults to the primary kind. Roles are
never deleted; move one between kinds with `/setup-meerkat` (`route-role.mjs`).

`install.sh` ships no roles root and never creates, writes or deletes one; a forced reinstall
carries an existing `roles/` across untouched. A copy of the director, chat console or setup
command left on a non-primary kind by an older install is kept, not refreshed.

The repository-root Claude marketplace `.claude-plugin/marketplace.json` lists a single
plugin, `./` (the board); Codex reads the same marketplace manifest, which is why it has no
bundle of its own.

Every agent above has both routes: a `--target` that lands the skill on disk, and a
plugin/bundle that agent's own plugin system reads in place.

`--link` applies to the **board** skill only. The director, chat console and role skills are
always copied: they are small and standalone (docs plus decisions), and a symlink would put a
checkout into the load path of a skill meant to read the same in every install. The
supervisor's execution body is not in the skill at all — it is `taskctl loop …`, shipped
with the runtime tree.

**Every agent kind gets a `taskctl` shim**, at `<agent-dir>/bin/taskctl`
(`~/.claude/bin/taskctl`, `~/.codex/bin/taskctl`, `~/.agents/bin/taskctl`, …), and it injects
**both halves of the identity** on every call — `--agent <name>` and `--actor-kind <kind>` —
with `TASKCTL_AGENT` / `TASKCTL_ACTOR_KIND` as the fallbacks. It is not the supervisor and
not a scheduling unit: it is one wrapper around a command the agent already runs itself,
which is why every agent kind — including OpenClaw, which has no loop — gets it.
Without it a write from that agent kind carries a name and no kind, and the board reads a write
with no kind as a **person's** — which is what would put it on the record as a decision.

**A `taskctl` already on `PATH` is adopted, with a backup.** The shell's `PATH` is not the
units' `PATH`: a `taskctl` left there by an earlier tool answers every bare call first and
writes that tool's identity. So an install resolves `command -v taskctl` and, when it names
a foreign shell script **inside the target home**, copies it to `<path>.bak-<timestamp>` and
writes this install's shim in its place. It leaves alone a symlink, a non-script, a file
outside the target home (a `--prefix` install must not touch the operator's real home), and
a shim already written by a meerkat install — the first agent kind in a run wins, so
`--target all` does not end with whichever kind installed last. `--dry-run` reports the
adoption and writes nothing.

The two bundle agent kinds write more than the skill. **Claude** additionally writes
`~/.claude/settings.json` (merged `env.TASKCTL_AGENT`, `env.TASKCTL_ACTOR_KIND`,
`env.TASKCTL_HUMAN` + a `SessionStart` hook),
`~/.claude/hooks/meerkat-taskpanel-session-start.sh`, `~/.claude/commands/{board,claim,deliver}.md`
and `~/.claude/bin/taskctl`. **Codex** writes `~/.codex/AGENTS.md` (a claim-first snippet),
the scheduling units under `~/.codex/scheduling/`, and `~/.codex/bin/taskctl`.
(Their shims are the same file described above — every agent kind gets one.) The bundle writes
only Claude's `SessionStart` hook; setup-meerkat's optional worktree round may merge
`PreToolUse` guard entries (and a Codex `hooks.json` entry) afterward.

The kind is not decoration. It is what the board reads to tell a person from an agent:
a human's comment resumes an `awaiting` card (any kind), and a `decision` / `confirm` /
`change` is what goes on the record (I5) and what the patrol counts as the leader acting.
A shim that set only the name recorded every agent write as a human's.

`--as-human` is the other direction — the one write an agent makes in the user's voice,
signing with `TASKCTL_HUMAN` instead of itself. `--actor-kind human` is refused: it is the
value every one of those decisions turns on, so a caller may not declare it.

### The local `projects.json` (one per install, at the config root)

The board holds no project directories, so the map from a **board project** to the
directory its **workers** run in is `<config root>/projects.json` — the program root
(the checkout, `/app`, or `~/.meerkat-taskpanel`), or `MEERKAT_TASKPANEL_CONFIG_DIR`
when set. It sits beside the per-kind configs (`.env.<kind>`) and is **one file for every
agent kind**: two agents on one installation share it.

It is maintained **only through `taskctl project map`** — `list`, `show <id>`,
`set <id> --local-path <dir>` / `--run-dir <dir>` / `--no-dir`, and `remove <id>` — and
read through `taskctl project dir <id>`. The reference,
[`projects-json.md`](../../skills/meerkat/references/projects-json.md), documents the schema
and the validation codes: an entry names the board `project`, may name a directory
(`local_path`, or the `run_dir` launch directory for a project that has none of its own),
and may carry other keys the CLI copies through unchanged. Those commands are
local: they send nothing to the board.

`install.sh` ships **no** `projects.json` and only ever writes one by migrating an older
copy (see below). It is never overwritten by a re-install — the same "merge, never
clobber" rule as the rest of the install — and it is git- and docker-ignored at the root
because it holds host paths.

**Upgrading from the skill-beside copy.** An older install kept the map inside each
installed board skill (`<agent-dir>/skills/meerkat/projects.json`) and could point at
another file with `MEERKAT_TASKPANEL_PROJECTS_JSON` / `--projects-json`. Both are retired.
The next `install.sh` merges every such file into the config root (the config-root entry
wins on the same project, source-only entries are added), removes the old file, and strips
a leftover `MEERKAT_TASKPANEL_PROJECTS_JSON` line from `.env` / `.env.<kind>` — the loader
would otherwise refuse the unknown key. A file that is not the documented shape is left in
place with a warning; repair it with `taskctl project map set`.

### The local `roles.json` (one per install, at the config root)

The role **catalog** is the host-local projection of the roles the supervisor discovered:
`<config root>/roles.json`, beside `projects.json` and the per-kind configs. It is written only by
`taskctl role sync --file <path|->` (the supervisor's poll publishes the discovered set on every
tick, a full replacement) and read by `taskctl role list` (the valid `assignee` range). Both are
local commands: no board request, no token. Identical content is a no-op; a malformed file is
reported and treated as absent, and the next sync repairs it.

It is **not board state**: there is no table, route or event, and no cross-machine sync. It is
git- and docker-ignored at the root. `install.sh` ships none and never writes one; the first
supervisor tick creates it. The loop's own writer identity is separate — `--agent` in the
scheduling units / `TASKCTL_AGENT` in the shim — and a name with no role skill is not
claimable.

### Scheduling units (claude, codex, pi)

| Artefact | Where | What |
|---|---|---|
| director skill | `<primary-agent-dir>/skills/meerkat-director/` | the decision hub as a skill: its `SKILL.md` plus the decision policy and operator view. The execution body is `taskctl loop …` in the runtime tree, not a copy. Always **copied**, never symlinked; the runtime finds it wherever it is installed |
| chat skill | `<primary-agent-dir>/skills/meerkat-chat/` | the human's console as a skill: its `SKILL.md` plus the console, answering and acceptance references. It carries no execution body; every write is the `meerkat` board skill's command. Always **copied**, never symlinked |
| setup skill | `<primary-agent-dir>/skills/setup-meerkat/` | `/setup-meerkat`: detect, repair, and move a role between kinds. Always **copied**, never symlinked |
| role skills | `<agent-dir>/skills/<role>/` for every discovered role (the standard five are jobs, linus, turing, reviewer, worker) | each role's workflow, `role.json` and references. The list is data (`scripts/install/lib/role-skills.mjs`), not a hard-coded set. Always **copied**; where a role is installed is the platform that runs it |
| loop state | `<agent-dir>/meerkat-taskpanel/` | the loop's runtime state — queue, ledger, locks, `sessions.state.json`, worker transcripts, `continue.state.json` (see [`scheduling.md §The loop's state directory`](../scheduling.md#the-loops-state-directory)) |
| launchd | `<agent-dir>/scheduling/launchd/com.meerkat-taskpanel.{session,ping,continue,subscribe}.plist` | `KeepAlive` for the resident pool and the event subscriber; `StartInterval` 60 / 600 for the two clocks |
| systemd | `<agent-dir>/scheduling/systemd/meerkat-taskpanel-{session,ping,continue,subscribe}.{service,timer}` | `Type=simple` + `Restart=always` for the pool and subscriber; 60s / 600s timers for the clocks |
| cron | `<agent-dir>/scheduling/cron/meerkat-taskpanel.cron` | `* * * * *` ping, `*/10 * * * *` continue — **no pool**: no worker dispatch |

Each unit names the **absolute `node`** that rendered it and carries a **`PATH`** holding
the agent **shim's** directory, the agent CLI's, and node's ahead of the scheduler's own
default. Neither is optional: a scheduler starts a unit with a minimal `PATH` that holds no
nvm and no Homebrew directory, so `/usr/bin/env node` is exit 127 — and the resident
session's `exec claude`, and the worker's own tool tree (including `taskctl`), resolve on
that same set. The shim heads the list so a globally installed `taskctl` cannot shadow it
and write the wrong actor identity. If the agent CLI is not on `PATH` at install time, the
installer says so and renders the shim directory and node's alone rather than guessing.
See [`scheduling.md` §What the loop's units run with](../scheduling.md#what-the-loops-units-run-with).
| per-kind config | `<program root>/.env.<actor-kind>` | the loop's policy: claiming, stale recovery, where `taskctl` lives (`MEERKAT_TASKPANEL_REPO`), the workflow channel, the clocks, the concurrency cap, the session pool and the role kind preference (`MEERKAT_TASKPANEL_AGENT_KIND_ORDER`). **Every key, its default and its allowed values are inventoried once in [`.env.example`](../../.env.example)** (`docs/tech-spec.md` TS-26). Full tool access is off unless both `MEERKAT_TASKPANEL_ALLOW_ALL_TOOLS=yes` and `..._ALLOW_ALL_TOOLS_REASON="…"` are set |
| log (runtime) | `<program root>/logs/supervisor.log` | the supervisor's tick log — **one file for every actor kind**, each line carrying `actor_kind=<kind>`; `~/.meerkat-taskpanel/logs/supervisor.log` for a combined install |
| state (runtime) | `<agent-dir>/meerkat-taskpanel/supervisor.state.json` | the running registry (legacy one-shot path) |
| state (runtime) | `<agent-dir>/meerkat-taskpanel/{sessions,subagents}.state.json` | the resident pool's session ids + heartbeat, and the subagent ledger |
| state (runtime) | `<agent-dir>/meerkat-taskpanel/subscribe.state.json` | the event daemon's pid, last connection/event and pending reconnect — what `--supervisor=status` reports |

`<agent-dir>` is `~/.claude`, `~/.codex` or `~/.agents`. **OpenClaw gets none of these, and no
`meerkat-director`, `meerkat-chat` or role skills** — it installs the board skill only and uses the
[openclaw-team](https://github.com/tanyu216/openclaw-team) framework's own
`poll`/`patrol` instead (see [`scheduling.md`](../scheduling.md#openclaw-no-loop-units)).

**Load one of them to start the supervisor** — this is the **manual** path, and a plain
install stops here (nothing is loaded for you):

```bash
# macOS
launchctl load ~/.claude/scheduling/launchd/com.meerkat-taskpanel.session.plist  # the resident pool
launchctl load ~/.claude/scheduling/launchd/com.meerkat-taskpanel.ping.plist    # and .continue.plist + .subscribe.plist
# Linux (user units)
mkdir -p ~/.config/systemd/user && cp ~/.claude/scheduling/systemd/* ~/.config/systemd/user/
systemctl --user enable --now meerkat-taskpanel-session.service meerkat-taskpanel-ping.timer meerkat-taskpanel-continue.timer meerkat-taskpanel-subscribe.service
# Anywhere — no long-lived process, so patrol + workflow only (no worker dispatch)
crontab ~/.claude/scheduling/cron/meerkat-taskpanel.cron
```

`--supervisor=enable` is the **opt-in** path that runs the right one of these for you —
see [Enabling the supervisor (opt-in)](#enabling-the-supervisor-opt-in) below.

### Enabling the supervisor (opt-in)

A plain install **writes** the units above and prints the load command — it never loads
anything. Starting the daemon is a separate, **explicit request**, and it is one flag:

```bash
bash install.sh --target all --supervisor=enable        # the FIRST loop kind in --target (claude)
bash install.sh --target pi,claude --supervisor=enable  # first in target order: pi
bash install.sh --target all --supervisor=enable --supervisor-actor-kinds claude,codex  # refused: one supervisor
bash install.sh --target all --supervisor=status        # every loop kind in --target
bash install.sh --target all --supervisor=disable       # stop every loop kind in --target
```

**One supervisor per machine is the rule** (see
[`scheduling.md §One supervisor per machine`](../scheduling.md#one-supervisor-per-machine)).
`--supervisor=enable` acts on the **first loop kind in normalized `--target` order** — the
**primary kind**; `--target all` therefore enables `claude` only. The primary kind is also
where the team skills land (director, chat, setup and a fresh install's roles); see
[Where things land](#where-things-land). Other platforms join by installing a role skill
there, not by starting a second supervisor, and `--supervisor-actor-kinds` naming several
kinds is refused with that hint. `--supervisor-actor-kinds` selects the **action's** kind; it
does not move the team skills.
`--supervisor=disable` and `--supervisor=status` cover **every** loop kind in `--target`, so
a pre-rule multi-supervisor machine is visible in one status and stopped by one disable.

**The director is found, not configured.** At session open the supervisor scans
`<home>/{.claude,.codex,.agents}/skills/meerkat-director/` (primary kind first, then
`MEERKAT_TASKPANEL_AGENT_KIND_ORDER`) and opens the director lane on the kind that carries
the skill. No `.env` key, `projects.json` field or constants entry records its location.

**`--supervisor-actor-kinds` names one primary kind for `enable`.** Listing several kinds
for `enable` is refused (one supervisor process already holds every kind's sessions; a
second would double the cadences, pools, logs and SLA ledgers). For `disable` and `status`
an explicit list still narrows the action, and both default to every loop kind in
`--target`. A kind outside `--target` is named and dropped (its installer never runs). The
flag selects *which* kinds an action applies to, rather than naming the kind the installer
itself runs as, which is why it is not called `--actor-kind` here: that name is the
per-command identity flag (`taskctl loop … --actor-kind claude|codex|pi`). **`openclaw` is rejected** in this list
(reason on stderr, exit 2) — it has no loop supervisor to enable (see
[`scheduling.md §OpenClaw: no loop units`](../scheduling.md#openclaw-no-loop-units)).

**One entry point, three actions.** `--supervisor=enable|disable|status` (aliases
`--enable-supervisor`, `--disable-supervisor`, `--supervisor-status`), with
`--scheduler auto|launchd|systemd|cron` choosing the mechanism — there is no second script
to remember. Non-interactive callers use
`MEERKAT_TASKPANEL_SUPERVISOR=enable|disable|status` instead of the flag (an explicit flag
wins), the same precedence style as `MEERKAT_TASKPANEL_CLAIM_UNASSIGNED`.

**The TTY question.** When stdin is a TTY and neither a flag nor the env var was given, the
installer asks once:

```text
Enable the supervisor now? It runs headless in the background (ping 60s / continue 600s). [y/N]
```

The default is **N** — deliberately the opposite of the `--claim-unassigned` question
below, which asks the same way but defaults **Y**. The supervisor is a **daemon** that runs
unattended, so silence must mean *no*: it is opt-in, never silent residency, and a plain
`install.sh` never enables it. The [interactive wizard](#interactive-wizard) asks the same
question (default `none`) as part of its one flow; when it has run, this standalone prompt
is skipped.

**Idempotence and safety.** A repeat enable never double-loads — an already-loaded unit with
an **unchanged** definition is reported as `already enabled` and exits 0 — and enabling a
second mechanism for the same agent kind requires disabling the first, so the installer
never leaves two loaded. An already-loaded unit whose definition **changed** is a different
thing: a service manager reads a unit only when it loads it, so the running job would keep
the old flags (a stale `--agent` outranks the per-kind config forever). `enable` compares
the loaded definition with the file just written, takes the changed unit down and loads it
again, and reports `reloaded`; a definition it cannot compare (older `launchctl print`
formats) is left alone and reported, never guessed at. `--dry-run` always wins: with it,
`--supervisor=enable` prints the exact commands it would run and changes nothing. On
success the installer reports what it enabled **and how to stop it**:

```text
supervisor: enabled (kind=claude, scheduler=launchd, units=com.meerkat-taskpanel.<agent>.claude.session,.ping,.continue,.subscribe)
to disable:  install.sh --supervisor=disable --supervisor-actor-kinds claude
```

A reload says so, and names what it cycled:

```text
supervisor: reloaded (kind=claude, scheduler=launchd, units=…)
supervisor:   reloaded com.meerkat-taskpanel.<agent>.claude.session — the definition on disk had changed and a loaded unit is only read at bootstrap
```

**A plain install restarts what it would otherwise leave stale.** The installer still never
*loads*, *enables* or *disables* a unit on its own — starting a daemon stays the operator's
call. It does **restart** a unit that is already running when the runtime tree under it
changed: the server target copies `src/**` unconditionally, a running process keeps the code
it started with, and an install that refreshed the code and said nothing would leave a fix
silently inert. The decision is a content digest of `src/**` (`<tree>/.install-state.json`),
so an install that changed nothing bounces nothing:

- `taskd` — with the guard loaded, `launchctl kickstart -k` / `systemctl --user restart`
  re-runs it and the report carries the replacement's pid.
- the loop supervisor — `session` and `subscribe` are restarted when loaded; the two timers
  are left alone (one-shot, they read the new code on their next fire), and a stopped unit
  stays stopped.
- the local event hub — it has **no unit** and is started lazily, so it is stopped rather than
  restarted: the installed CLI runs `events hub --stop` (with board autostart disabled), and the
  next `events follow` / `export watch` starts one from the new tree. It runs **before** the
  taskd restart, because the stop resolves the board first and a board mid-restart would fail
  it. Detached registrations are persisted and rehydrated. The step is non-fatal, skipped by
  `--no-automation` and `--dry-run`, and by `MEERKAT_TASKPANEL_SKIP_HUB_STOP=1` (**for tests
  only**). A same-generation hub that is wedged is not replaced by an install;
  `taskctl events hub --stop` retires it.
- `cron` has no long-lived process, so there is nothing to restart.
- A restart can be requested explicitly with `MEERKAT_TASKPANEL_RUNTIME_CHANGED=1`, which an
  agent-only install cannot otherwise know.

Separately, a reinstall that *rewrote unit definitions* a manager already has loaded still
prints the stale report rather than "nothing is loaded for you" — that would be false. It
probes (read-only) and reports:

```text
supervisor: warning (kind=claude) — the scheduling units were just rewritten, but launchd still has the old definition loaded
supervisor:   the running supervisor keeps its old flags until it is reloaded (an old --agent outranks the per-kind config)
supervisor:   reload it with: install.sh --supervisor=enable --supervisor-actor-kinds claude
```

Enabling installs the resident pool, the two clocks **and the event daemon**: a long-lived
`taskctl loop session` manager, a $0 ping, one bounded tick per continue cadence that runs
patrol → workflow → poll in-process, and the `taskctl loop subscribe` daemon on launchd/systemd.
The daemon is part of that set, not a separate flag: it is the low-latency path, the
continue cadence remains the fallback, and an event wakes a turn only while the pool is
live. It is not a closure component — with no pool it skips the turn and the timer still
runs patrol + workflow — and `--supervisor=status` reports whether it is `streaming`,
`reconnecting` or `not running` from `subscribe.state.json`. To run without it, disable that
one unit in the service manager; `--event-continue=no` is the runtime soft-off. Inside that turn the poll is still the
$0 local scan; it claims and enqueues, and the pool opens or resumes the card's worker
session — so a worker turn still starts only when a claim succeeded (see
[`scheduling.md §The cost gate`](../scheduling.md#the-cost-gate-a-worker-turn-starts-only-on-fire)).
A cron install has no pool; it runs the clocks only, and dispatch stays paused.

The exact per-OS commands, their failure modes, and the `loginctl enable-linger` caveat are
in [`scheduling.md §Enabling the supervisor (opt-in)`](../scheduling.md#enabling-the-supervisor-opt-in).

An enable that the OS refuses — a `launchctl bootstrap` with no GUI session, a
`systemctl --user` with no D-Bus — is **reported, not disguised**: the install prints
`supervisor: not enabled (…)` with the reason and the exact command to run by hand, and
still exits 0, because the agent's skills are installed either way and an OS-level refusal
is not something `--force` fixes.


### The taskd guard (keepalive, always enabled)

The board server `taskd` (`src/server/main.mjs`) is kept alive by an **OS-level guard**,
and — alone among the units `install.sh` writes — it is **enabled at install time**.
`taskctl` lazy-starts `taskd` only when a CLI call needs it, so an idle board whose
server dies stays down until the next call (see
[`scheduling.md § The taskd guard (keepalive)`](../scheduling.md#the-taskd-guard-keepalive));
the guard closes that gap by restarting the server after any exit.

This is a deliberate, **narrow exception** to the rule that the installer never starts
a daemon for you (the [Scheduling units](#scheduling-units-claude-codex-pi) above). It
applies **only to the `taskd` service guard**: the `meerkat-director` supervisor's units are
still written and left for you to load, and OpenClaw still gets none.

**Where it lands** — under the install root, deliberately *not* the loop units'
`<agent-dir>/scheduling/`, so re-installing an agent kind's loop units never touches it:

```text
<prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
<prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service
```

`<prefix>` is `--prefix` (default `$HOME`). There is **one guard per machine** — and
therefore **one guarded board** — not one per agent kind: the label and unit name carry
no agent or actor-kind segment, and which board it serves is chosen at install time. See
[`scheduling.md § One guard per machine`](../scheduling.md#one-guard-per-machine).

**What install does:** it writes the unit and loads it immediately — no question, no
separate flag:

```bash
# macOS
launchctl bootstrap gui/$UID <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
# Linux (user unit)
mkdir -p ~/.config/systemd/user
cp <prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
systemctl --user enable --now meerkat-taskpanel-taskd.service
```

The install ends with a report line, followed by the commands that undo it:

```text
taskd guard: enabled (com.meerkat-taskpanel.taskd, pid …)
```

`enabled` is claimed only once the board itself answers `/health` — a loaded unit with a
pid is not a working board. When the unit is loaded but nothing answers, install says so
and names the log to read:

```text
taskd guard: loaded (com.meerkat-taskpanel.taskd, pid …); /health did not answer within 10000ms — read <dataDir>/logs/taskd.log
```

A board that is **already serving** — the runtime pointer's address answers `/health`
before the guard is loaded — is left alone. Loading a second `taskd` on that port is not a
keepalive; it is an `EADDRINUSE` restart loop. The units are still written, nothing is
loaded, and the install says:

```text
taskd guard: not loaded — a board is already serving http://127.0.0.1:9527 (pid …); stop it and re-run to install the guard
```

Neither line fails the install. The unit names the **absolute** node that ran the install,
so a node that lives only under nvm or Homebrew still starts the server — see
[`scheduling.md § The taskd guard (keepalive)`](../scheduling.md#the-taskd-guard-keepalive).

**Which program the unit runs.** With the server target selected (`--target server` /
`all`, which includes it, and always for an npm/npx install unless `--no-server`), the unit
runs the installed tree's
`src/server/main.mjs` with `TASKD_DATA_DIR=<home>/.meerkat-taskpanel/.data` pinned in its
environment — so the guarded board is the installed one, not whatever checkout you happened
to install from, and it keeps working after the checkout moves or the npx cache is pruned.
Without the server target the unit runs the checkout's `src/server/main.mjs` and leaves the
data directory to the normal config chain. A unit written for one layout does not match the
other, so re-pointing an agent-only install at a server install (or back) is a deliberate
`--force`.

**A second board does not inherit the guard.** The guard is one per machine
([`scheduling.md § One guard per machine`](../scheduling.md#one-guard-per-machine)), so a
re-install for a *different* board is refused rather than silently re-pointing the machine's
only guard (on Linux, systemd reads `~/.config/systemd/user/meerkat-taskpanel-taskd.service`
and that file is the guard). The refusal names both boards, leaves the unit alone, and is
the install's one non-zero guard outcome — **exit 3**, which `install.sh` reports and then
carries on from (the agent-kind skills are installed either way, and the install's own exit status
is unchanged). `--force` is how you re-point it deliberately:

```text
taskd guard: refused — com.meerkat-taskpanel.taskd is already loaded and serves another board (<its dataDir>);
  ~/.config/systemd/user/meerkat-taskpanel-taskd.service was left unchanged, and this board (<this dataDir>) is not guarded by it
taskd guard: this machine runs one guarded board — re-run with --force to point it at <this dataDir>
```

A re-install of the **same** board is not a refusal, and says which board the loaded guard
serves — so `already loaded (no change)` can no longer read as "your board is guarded" when
it is not:

```text
taskd guard: already loaded (no change) — the machine's guard serves this board (<dataDir>)
taskd guard: already loaded (no change) — which board it serves could not be read here
```

**Stopping / uninstalling it.** The guard restarts the server after *any* exit, so a
plain `kill` is not enough — use the supervisor's own stop:

```bash
# macOS
launchctl bootout gui/$UID/com.meerkat-taskpanel.taskd
rm <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
# Linux
systemctl --user disable --now meerkat-taskpanel-taskd.service
rm ~/.config/systemd/user/meerkat-taskpanel-taskd.service
```

**Internal bypasses** (not user options): `--no-automation` skips the guard along with
its other automation targets (see its row in the flag table below), and its environment
form `MEERKAT_TASKPANEL_NO_AUTOMATION=1` — the variable `install.sh` sets for each agent kind
installer — skips exactly the same things; `--dry-run`
writes and loads nothing and reports `taskd guard: skipped (dry-run)`;
`MEERKAT_TASKPANEL_SKIP_TASKD_SERVICE=1` skips the guard and is **for tests only**.

The unit files in full, the restart and back-off semantics, and how the guard coexists
with `taskctl`'s lazy start are in
[`scheduling.md § The taskd guard (keepalive)`](../scheduling.md#the-taskd-guard-keepalive).


### The claim-unassigned question

When stdin is a TTY and no flag was given, `install.sh` asks once:

```text
Allow claiming unassigned tasks? [Y/n]
```

The answer is persisted to `<program root>/.env.<actor-kind>` as
`MEERKAT_TASKPANEL_CLAIM_UNASSIGNED=yes|no`; the supervisor reads it as its default (an
explicit `--claim-unassigned` flag or `$MEERKAT_TASKPANEL_CLAIM_UNASSIGNED` overrides it).
The [interactive wizard](#interactive-wizard) asks this question inside its one flow;
when it has run, this standalone prompt is skipped.
Non-interactive installs default to **yes**; `--claim-unassigned=no` /
`--assignee-only` persist `no`. **Stale recovery is a separate opt-out**, resolved the same way:
`--recover-stale=no` (or `$MEERKAT_TASKPANEL_RECOVER_STALE=no`) persists
`MEERKAT_TASKPANEL_RECOVER_STALE=no`. The switch only controls whether the poll's
candidate read includes the stale-recovery pool (`issue candidates --stale`); the takeover
decision remains the board's, through `issue claim`:

| Threshold | Meaning | Enforced by |
|---|---|---|
| 10 minutes | The same holder may re-claim (refresh ownership/heartbeat). | `issue claim` |
| 30 minutes | The card enters the `issue candidates --stale` recovery pool. | candidate read |
| 6 hours | Any agent may take over, and the takeover writes a `change` comment. | `issue claim` |

With `--recover-stale=no` the supervisor never adds `--stale`, so it never claims an
`in_progress` card. The SLA patrol and its alerts still run — an alert is the only signal
for a stranded card. This switch does **not** affect the local handoff queue's
dead-process/timeout recovery (`recoverStaleCards` in `src/orchestrator/session.mjs`),
which runs regardless. The file is merge-style: a re-run without
`--force` keeps the existing value, and it also records where the board engine
lives (`MEERKAT_TASKPANEL_REPO=<runtime tree>`) — a key the installer adds when it is
missing, so an install made before that key existed still learns it (see
[`scheduling.md §The loop's state directory`](../scheduling.md#the-loops-state-directory)).

The same merge rule covers the workflow channel's key, written with its default when
the file is created and added only when missing on an upgrade:

```ini
MEERKAT_TASKPANEL_WORKFLOW=yes
```

Reviewer selection is **not** install or config state: the local role catalog's
`default_reviewer` owns it, and `MEERKAT_TASKPANEL_REVIEWER` is deprecated and ignored.

Acceptance is **not** install configuration: each card states its own mode
(`off | auto | human`) with `issue create --acceptance-mode …`. There is no project-level
default; a project may document its creators' guidance in `docs/agents/create-card.md`.
The file is merged, never rewritten.

`MEERKAT_TASKPANEL_ENV_FILE` points the installer and both binaries at a different board
file (a read-only package root, a container, or the test suite).
`MEERKAT_TASKPANEL_CONFIG_DIR` does the same for the machine-local config directory: each
kind is `<dir>/.env.<actor-kind>`, and the project map is `<dir>/projects.json`.

The SLA thresholds are seeded as a **commented** block in the same `.env` (every
`TASKD_SLA_*` key with its default). Uncomment a line to override it: `taskd` validates the
table and publishes it on `GET /api/v1/sla`, and the loop reads it on the next patrol pass —
no restart and no per-agent-kind copy. See [`sla.md §2.1`](../sla.md#21-install-level-overrides).

Review dispatch follows the local role catalog: the review card's assignee is its
`default_reviewer`. With no reviewer-lane role installed the supervisor still reconciles
containers and applies verdicts already in flight — it simply creates no new review cards, and
deliveries wait for a human.
[`scheduling.md §The workflow channel`](../scheduling.md#the-workflow-channel-review-acceptance-and-containers)
is the full contract.

### Interactive wizard

`npx meerkat-taskpanel`, the globally installed `meerkat-taskpanel`, and
`bash install.sh` all run the same installer, so they all offer the same wizard.
With **no options** on a terminal it starts automatically; `-i` / `--interactive`
forces it (including with piped answers), and `--yes` / `--non-interactive` skips
it. Piped input, `--dry-run` and `--yes` never prompt, so CI keeps the defaults.

The wizard asks, in order — every question shows a default and Enter accepts it:

1. install root (the server tree and agent skills live under it)
2. install mode — full (server + agent skills), skills only, or server only
3. agent kinds (only for full / skills only): claude / codex / pi / openclaw
4. your name on the board (default: the name an earlier install recorded, else the OS user)
5. link or copy the board skill (only when an agent is selected)
6. build `web/dist` if missing (only when the server is selected)
7. claim unassigned / recover stale / workflow / continue switch
8. supervisor action (default `none`) and, if one is chosen, the scheduler

With `--no-server`, the mode question is skipped: it is skills-only by
definition, and the agent-kind question follows.

The board-name answer is exported as `MEERKAT_TASKPANEL_HUMAN_NAME` — the name
`--as-human` signs with. It is asked on every run: agent installs render it into
the shim/settings, and a server install records it as `TASKCTL_HUMAN` in the
tree's `.env`. `taskctl` reads that key, and `taskd` uses it as the default actor
for a request that carries no `X-Taskctl-Actor` header (the browser UI, `curl`);
with the key unset they stay `human:local`.

An answer is not the only source of the name. When nothing supplies one, an
install **keeps the one already recorded** — `TASKCTL_HUMAN` in the board's
`.env` first, else the shim's baked default — and only a machine with no record
falls back to the OS user. A `--force` reinstall therefore never re-signs the
operator's decisions under a second name; to change it, say so (wizard answer or
`MEERKAT_TASKPANEL_HUMAN_NAME`), which always wins.

**Agent name and the ping/continue intervals are not asked.** Their
defaults are fixed (`Worker` / `60` / `600`); edit
`<program root>/.env.<actor-kind>` (or `TASKCTL_AGENT` in the agent settings) to
change them. The reviewer is not configurable: the installed role catalog's
`default_reviewer` owns it.

Answers feed the same resolution path as the flags, so everything below still
applies. `q`, `quit`, `cancel` or EOF aborts with exit 2 **before** anything is
written; Ctrl-C is the shell's 130. In `--interactive` mode a supplied flag is
shown as the default and typing a different answer overrides it.

The wizard covers the installation shape and loop policy only. SLA/patrol settings
(patrol cadence, `--patrol-llm`, notification commands) are not asked; they remain
managed by the loop's own configuration and CLI.

## install.sh reference

```text
install.sh [--target claude|openclaw|codex|pi|server|all] [--prefix <home>]
           [--agent-name <name>] [--no-automation]
           [--claim-unassigned=yes|no] [--assignee-only] [--recover-stale=yes|no]
           [--workflow=yes|no] [--reviewer <agent>]
           [--ping-interval <sec>] [--continue-interval <sec>] [--continue=yes|no]
           [--supervisor=enable|disable|status] [--supervisor-actor-kinds <list>]
           [--scheduler auto|launchd|systemd|cron]
           [--link] [--force] [--dry-run] [--skip-node-check] [--skip-web-build]
           [-i|--interactive] [--yes|--non-interactive] [-h|--help]
```

| Flag | Effect |
|---|---|
| `--target <agent-kind>` | Which agent kind(s) to install for, or `server` for the runtime tree. Default `all` — every agent plus the server. Repeatable, and a comma-separated list (`--target claude,server,pi`) works too. The **first** `claude|codex|pi` in the list is the primary kind and receives the team skills; the board skill goes to every selected kind. `--no-server` removes the server from whatever the selection resolves to. |
| `--no-server` | Install the agent-kind skills only, leaving the runtime tree out (`--target all --no-server` is the old `all`). |
| `--prefix <home>` | Install relative to `<home>` instead of `$HOME`; the server tree lands at `<home>/.meerkat-taskpanel`. |
| `--skip-web-build` | Never try to build `web/dist`. A server install without a prebuilt `web/dist` is then API-only (with a warning) instead of building it. |
| `--agent-name <name>` | The agent's identity on the board, written into the claude/codex bundle and rendered into every scheduling unit as `--agent <name>` (default `Worker`). It is the loop's **writer** identity, not its claim set: what the loop claims for is every discovered worker-lane role plus every discovered reviewer-lane role. |
| `--supervisor-actor-kinds <kind>` | With `--supervisor=enable`, which loop kind is the **primary** (one per machine). The normalized `--target` order, primary first, becomes `MEERKAT_TASKPANEL_AGENT_KIND_ORDER`; a kind the order omits is appended at discovery time, never excluded. |
| `--supervisor=enable\|disable\|status` | Apply the supervisor action per loop kind. `status` prints the discovered scan set (worker roles + reviewer-lane roles) and the machine view. |
| `--no-automation` | Skip the Codex scheduling units **and** the `taskd` keepalive guard; the Claude/OpenClaw/Pi skill bundles are otherwise unaffected. |
| `MEERKAT_TASKPANEL_NO_AUTOMATION=1` | The environment form of the `--no-automation` bypass — `install.sh` sets exactly this variable for each agent-kind installer, and `scripts/install/_common.sh` reads it, so a direct installer run honours it too. Internal bypass, not a user option. |
| `--claim-unassigned=yes\|no` | May the supervisor claim **unassigned** cards (the public pool)? Persisted to `<program root>/.env.<actor-kind>`; overrides the interactive answer. Default `yes`. |
| `--assignee-only` | Alias for `--claim-unassigned=no`. |
| `--recover-stale=yes\|no` | Add stale `in_progress` cards to the poll's candidate read (`issue candidates --stale`). The takeover itself is still `issue claim`, decided by the board (holder after 10 minutes, anyone after 6 hours). Persisted to the per-kind config; default `yes`. |
| `--workflow=yes\|no` | Run the supervisor's workflow sub-pass — ask the board for the workflow plan (`issue reconcile --workflow`) and apply its review actions. Persisted to the per-kind config; default `yes`. `no` means the loop writes no card at all. |
| `--reviewer <agent>` | Deprecated and ignored; a review card's assignee selects the reviewer role from the installed catalog. Accepted so an older unit or script does not fail. |
| `--ping-interval <sec>` | The liveness clock, rendered into the units; default `60`. It only prunes dead worker pids — no board read, no LLM. |
| `--continue-interval <sec>` | The judgment clock, rendered into the units; default `600` (10 minutes). It wakes the agent kind with a bounded `Continue` prompt; that turn runs poll, patrol and workflow. |
| `--continue=yes\|no` | Enable the continue turn. Persisted to the per-kind config; default `yes`. `no` keeps the ping clock but discovers no work until a human runs the poll entry. |
| `--supervisor=enable\|disable\|status` | Opt-in control of the supervisor **daemon** (the units above): `enable` loads/enables it, `disable` unloads/removes it, `status` prints each agent kind's load state, platform and discovered scan set (worker roles + reviewer-lane roles), plus a machine-level view of every loaded loop kind, and changes nothing. Off by default — a plain install still only *prints* the load command. Aliases: `--enable-supervisor`, `--disable-supervisor`, `--supervisor-status`. See [Enabling the supervisor (opt-in)](#enabling-the-supervisor-opt-in). |
| `--supervisor-actor-kinds <list>` | Which loop agent kinds the `--supervisor` action applies to — a comma list ⊆ `{claude,codex,pi}`. For `enable` it must name exactly one kind (the primary kind); listing several is refused because one supervisor already holds every kind. `disable`/`status` default to every loop kind in `--target` and accept a narrowing list. A kind outside `--target` is named and dropped. Not `--actor-kind`: that is the loop's per-command identity flag. `openclaw` is **rejected** (reason on stderr, exit 2). |
| `--scheduler <auto\|launchd\|systemd\|cron>` | Which mechanism the `--supervisor` action acts on (default `auto`: launchd on macOS, systemd on Linux, cron elsewhere). Only one mechanism may be loaded per agent kind — see [`scheduling.md §Enabling the supervisor (opt-in)`](../scheduling.md#enabling-the-supervisor-opt-in). |
| `MEERKAT_TASKPANEL_SUPERVISOR=enable\|disable\|status` | The non-interactive form of `--supervisor` (an explicit flag wins) — the same precedence style as `MEERKAT_TASKPANEL_CLAIM_UNASSIGNED`. |
| `--link` | Symlink the **board** skill instead of copying it — edits to the repo take effect immediately. Recommended for a checkout. The director, chat console and role skills are always copied (standalone docs + decisions). For `--target server` it only warns: the runtime tree is always copied, because it must survive the checkout and the npx cache. |
| `--force` | The **only** way to overwrite. Without it, an existing destination is left untouched (merge, never clobber) and the installer prints a `left unchanged — use --force to overwrite` notice, so a re-run is safe, idempotent, and exits 0. With it, managed files and managed config keys are re-rendered from the current flags — but operator data is kept: an installed `roles/` directory, an existing config-root `projects.json`, config keys the template does not carry, and the server `.env` / `.data`. An old skill-beside map is migrated into the config root rather than kept beside the skill. |
| `--dry-run` | Print every path that would be written/changed and change nothing. It never prompts, even with `--interactive`. |
| `--skip-node-check` | Skip the Node version check (see below). |
| `-i, --interactive` | Ask every option in the [wizard](#interactive-wizard). Also works when stdin is a pipe, which is how it is tested; EOF cancels. |
| `--yes, --non-interactive` | Accept every default and never prompt — the CI form. Wins over `--interactive`; it never enables the supervisor by itself. |

**The `taskd` guard is always enabled and has no user flag.** Every install writes
the `taskd` keepalive unit and loads it — there is no switch to turn it on and no prompt
to answer (see [The taskd guard](#the-taskd-guard-keepalive-always-enabled)). The
bypasses are `--no-automation` (or its environment form
`MEERKAT_TASKPANEL_NO_AUTOMATION=1`), `--dry-run`, and — **for tests only** —
`MEERKAT_TASKPANEL_SKIP_TASKD_SERVICE=1`. This is the one exception to "write the unit,
print the load command, never start a daemon"; the supervisor's units keep that rule.

Before dispatching, `install.sh` requires **Node >= 22** — the engine stores to SQLite
through the built-in `node:sqlite` module, which the 22.x line is the first to ship. A
missing or older `node` is a hard error (`exit 2`), so a machine that cannot run the board
never gets a "successful" install; `--skip-node-check` bypasses the check. The same check
runs from `scripts/install/_common.sh`, so a direct `scripts/install/<agent-dir>.sh` invocation
is guarded too.

`install.sh` is a thin dispatcher: it parses the flags once, then runs
`scripts/install/<actor-kind>.sh` for each target and prints a per-agent-kind summary. It
exits non-zero if any agent kind failed, and each failure names the destination and the fix.

### Idempotence: re-running and `--force`

Installing is **merge-style and idempotent**, so a re-run is never destructive and
never an error:

- **Re-running exits 0** even when nothing changed — an existing install is the desired
  end state, not a conflict.
- **Managed files and managed config keys keep their existing value by default.** The
  skill directory, the emitted scripts (hook / trigger / shim / slash commands), the
  `AGENTS.md` snippet, `env.TASKCTL_AGENT` / `env.TASKCTL_ACTOR_KIND` / `env.TASKCTL_HUMAN`,
  and the `SessionStart` hook are all left
  exactly as they are, and the installer reports each as unchanged — for an emitted file,
  `left unchanged — use --force to overwrite`. This means changing a flag such as
  `--agent-name` on its own does **not** take effect on an existing install — it still
  exits 0, with only the unchanged notices on stdout. The loop's **writer** identity is the
  `--agent` rendered into each scheduling unit (and `TASKCTL_AGENT` in the shim);
  `--agent-name` is what that value comes from. The claim set is discovered, not configured.
- **`--force` is the only way to overwrite.** With it, the managed files and managed
  config keys are re-rendered from the current flags (unrelated keys and hooks are still
  preserved). Deleting the destination first also gives a clean slate.

> One cosmetic exception: re-rendering the Codex `AGENTS.md` snippet with `--force` adds
> exactly one trailing blank line (the sha256 changes; the rendered block and any
> pre-existing content do not). A plain re-run without `--force` is byte-identical, and a
> second `--force` is stable.

### Examples

```bash
bash install.sh --target all --dry-run                 # preview
bash install.sh --target claude --link --force         # development install
bash install.sh --target all --prefix /tmp/tp-home     # isolated home (testing)
bash install.sh --target all --supervisor=enable       # opt in: install + load one loop supervisor (claude)
bash install.sh --target all                           # full install: runtime tree + every agent
bash install.sh --target server --skip-web-build       # API-only server, no frontend build
```

## Per-agent-kind steps

### Claude Code

```bash
# 1. skill + agent bundle (settings.json, slash commands, shim)
bash install.sh --target claude --link --agent-name alice

# 2. plugin (registers the marketplace, then installs the plugin)
claude plugin marketplace add "$PWD"
claude plugin install meerkat-taskpanel@meerkat-taskpanel-marketplace -y

# verify
claude plugin list                  # → meerkat-taskpanel@meerkat-taskpanel-marketplace, enabled
claude plugin validate "$PWD"       # validates the plugin + marketplace manifests
```

Step 1 writes, merge-style and idempotently: the skill, `~/.claude/settings.json`
(`env.TASKCTL_AGENT=alice` + a `SessionStart` hook that starts the board and lists
claimable cards), `~/.claude/commands/{board,claim,deliver}.md`, the `taskctl` shim at
`~/.claude/bin/taskctl`, and the scheduling set (`~/.claude/scheduling/*`). The per-kind
config is separate: `<program root>/.env.claude` (see
[Where the per-kind config lives](#upgrading-and-removing)). Re-running is safe: existing
`settings.json` keys and hooks are preserved, and `--force` is the only way to overwrite.

Uninstall:

```bash
claude plugin uninstall meerkat-taskpanel@meerkat-taskpanel-marketplace   # if you installed it
claude plugin uninstall meerkat-taskpanel@meerkat-taskpanel-marketplace
claude plugin marketplace remove meerkat-taskpanel-marketplace
rm -rf ~/.claude/skills/meerkat ~/.claude/skills/{meerkat-director,meerkat-chat} ~/.claude/skills/{jobs,linus,turing,reviewer,worker} \
       ~/.claude/skills/setup-meerkat \
       ~/.claude/commands/{board,claim,deliver}.md \
       ~/.claude/hooks/meerkat-taskpanel-session-start.sh ~/.claude/bin/taskctl \
       ~/.claude/scheduling ~/.claude/meerkat-taskpanel
```

### OpenClaw

```bash
# 1. skill
bash install.sh --target openclaw --link --force

# 2. bundle — OpenClaw consumes the Claude-format bundle at the repository root
openclaw plugins install "$PWD" --force --accept-capabilities

# verify
openclaw plugins inspect meerkat-taskpanel
```

`openclaw plugins inspect meerkat-taskpanel` reports the bundle mapping:

```text
Format: bundle
Bundle format: claude
Bundle capabilities: skills
```

OpenClaw gets **no per-kind config** from the installer (it runs openclaw-team's own
supervisor). A hand-created `<program root>/.env.openclaw` is still read by `taskctl` as a
per-agent default layer, and the file name follows the same pattern.

**Why `claude`?** The repository root carries `.claude-plugin/`, and OpenClaw's detection
precedence is **codex → cursor → claude**; with no Codex or Cursor marker present, the root
is reported as a claude bundle.

> **No supervisor for OpenClaw.** Unlike the other three agent kinds, OpenClaw gets **no**
> scheduling unit and **no `meerkat-director`, `meerkat-chat` or role skills**: it already has a scheduler in the
> [openclaw-team](https://github.com/tanyu216/openclaw-team) framework, whose
> per-agent poll and `task-patrol` automations *are* the design in
> [`scheduling.md`](../scheduling.md). `install.sh --target openclaw` writes the board skill
> only — install that framework instead. The exclusion is permanent, not a gap: two
> supervisors on one board would only race each other for the same cards.

> **No native manifest.** Meerkat TaskPanel does not ship an `openclaw.plugin.json`. A
> *native* OpenClaw plugin is an in-process runtime module and must ship
> `package.json#openclaw.extensions` plus an entry point that imports the OpenClaw plugin
> SDK — a real dependency this zero-dependency project deliberately does not take, so such
> a manifest could not be installed as what it claims to be. The **skill-only** pack
> installs through the supported bundle route above (or, equivalently, by placing the skill
> in `~/.openclaw/skills/`). OpenClaw's detector does recognise a native manifest when one
> is present; the repository simply has none.

Uninstall:

```bash
openclaw plugins uninstall meerkat-taskpanel
rm -rf ~/.openclaw/skills/meerkat
```

### Codex

```bash
# 1. skill + agent bundle (AGENTS.md snippet, scheduling units, shim)
bash install.sh --target codex --link --agent-name alice

# 2. plugin
codex plugin marketplace add "$PWD"
codex plugin add meerkat-taskpanel@meerkat-taskpanel-marketplace

# verify
codex plugin list
```

Step 1 writes, merge-style and idempotently: the skill, a claim-first snippet appended to
`~/.codex/AGENTS.md` (existing content is preserved), the `taskctl` shim at
`~/.codex/bin/taskctl`, and the scheduling set (`~/.codex/scheduling/*`). The per-kind
config is separate: `<program root>/.env.codex`. The bundle writes no Codex hooks (Codex 0.161+
supports them; setup-meerkat's optional worktree round may add a guard adapter), so scheduling
is the auto-claim mechanism — load one of the units (see
[Scheduling units](#scheduling-units-claude-codex-pi)). `--no-automation` (or its environment
form `MEERKAT_TASKPANEL_NO_AUTOMATION=1`) skips the scheduling set.

Uninstall:

```bash
codex plugin remove meerkat-taskpanel
codex plugin marketplace remove meerkat-taskpanel-marketplace
rm -rf ~/.codex/skills/meerkat ~/.codex/skills/{meerkat-director,meerkat-chat,setup-meerkat} ~/.codex/skills/{jobs,linus,turing,reviewer,worker} \
       ~/.codex/bin/taskctl \
       ~/.codex/scheduling ~/.codex/meerkat-taskpanel
```

### Pi / Agent Skills

```bash
# 1. skill + scheduling set (units; per-kind config at <program root>/.env.pi)
bash install.sh --target pi --link --force

# 2. package — the repository's own package.json declares the pi manifest
pi install "$PWD"

# verify
pi list
```

The root `package.json` carries Pi's manifest, so no copy of the skill tree is made:

```jsonc
"pi": { "skills": ["./skills"], "extensions": ["./.pi/extensions/meerkat-taskpanel.ts"] }
```

`pi.skills` is the skills directory, and `pi.extensions` is the module that re-registers
the same directory at runtime through Pi's `resources_discover` event. Those are two of the
four resource fields Pi reads from a package's `pi` object (`extensions`, `skills`,
`prompts`, `themes`), which is why the board needs no Pi bundle directory of its own. The
same extension forwards `tool_call` to `taskctl worktree guard --runtime pi`; a project opts
into the guard with `docs/agents/worktrees.md` (setup-meerkat's optional worktree round).

Pi has no timer of its own, so step 1 also drops the units under
`~/.agents/scheduling/`; load one of them to run the supervisor (see
[Scheduling units](#scheduling-units-claude-codex-pi)).

The loop runs one card per session and spawns nothing from inside one, so a Pi host needs no
subagent extension: the CLI, a selected provider and model, and the role skills are enough.

Pi also needs a **provider and model selected**. The loop does not pass `--provider` or
`--model`: Pi uses `defaultProvider` / `defaultModel` from `~/.pi/agent/settings.json` (the
TUI's model picker writes them). A provider that is only registered in
`~/.pi/agent/models.json` — for example by cc-switch — is available but not the default, so a
session started without an explicit selection fails at the provider (a 403 is typical). Set
the default once, then verify with:

```bash
pi --print --no-session "Reply with OK"
```

Uninstall:

```bash
pi remove "$PWD"
rm -rf ~/.agents/skills/meerkat ~/.agents/skills/{meerkat-director,meerkat-chat,setup-meerkat} ~/.agents/skills/{jobs,linus,turing,reviewer,worker} \
       ~/.agents/scheduling
```

## Agent integration: skill only

> **The MCP integration has been removed — agent integration is skill-only.** The stdio
> MCP server and its `meerkat-taskpanel-mcp` bin are gone. An agent kind left with an MCP
> registration should drop it — `claude mcp remove meerkat-taskpanel`, `codex mcp remove
> meerkat-taskpanel`, or the matching entry in OpenClaw's or Pi's MCP configuration. No
> replacement is needed: the skill drives the `taskctl` CLI directly.

The installed skill is self-contained but carries **no engine**: `install.sh` copies
`SKILL.md`, `references/` and `scripts/` into the agent directory, never `src/`. So the wrapper
`scripts/run.mjs` resolves the CLI on a ladder — a checkout named by
`MEERKAT_TASKPANEL_ROOT` (or by `MEERKAT_TASKPANEL_REPO`, the key the installer records in
the per-kind config), then a checkout around the skill itself, then **the agent kind's own `taskctl`
shim** beside the skill, then `taskctl` on `PATH` — and when none of those holds a CLI it
stops with the ways to fix it, instead of the module-not-found stack an earlier version died
with (T-20261014-214002). In the normal case it is the shim that answers: `install.sh` writes
it into the agent kind's own `bin/` (`~/.claude/bin/taskctl`, `~/.codex/bin/taskctl`, …), and the
ladder finds it **without** `PATH` having to include that directory — one fewer thing to get
right, and the rung that carries the identity. Putting the directory on `PATH` also works,
as does `npm link` or a global install.

## Verifying the install

**Everything that runs is verified inside the container** (see
[`docker.md`](../maintenance/docker.md) and ARCHITECTURE §9.1): the four agent CLIs are not installed in
the image and there is no network, so the container asserts each agent kind's *detection
contract* offline instead.

```bash
npm run verify:docker                     # the full container suite, all four agent kinds
```

That run includes `scripts/verify/profiles.mjs`, which for each agent kind installs into a
throwaway home and asserts: the skill lands at the agent kind's destination, `SKILL.md`
frontmatter still names `meerkat`, the wrapper resolves back to this checkout, the
manifest parses, and the bundle is recognized with a loadable skill. Run one agent kind
on its own with:

```bash
node scripts/verify/profiles.mjs --actor-kind openclaw
```

### Supplementary: the real agent CLIs

When the agent CLIs *are* present (a developer machine, never CI), the same four steps are
driven for real, each in a throwaway `HOME`, touching no network:

```bash
node scripts/verify/kind-cli.mjs            # all agent kinds; absent CLIs are SKIPPED
node scripts/verify/kind-cli.mjs --actor-kind openclaw
```

This is a cross-check that the offline detection rules match what the real CLI does — it
is supplementary, not the acceptance evidence.

### The runtime claim path (offline mock harness)

`profiles.mjs` proves the bundle *installed*; it never runs what the install generated.
`scripts/verify/kind-runtime.mjs` does — offline, with no agent CLI and no board:

```bash
node scripts/verify/kind-runtime.mjs            # all agent kinds; exit 0 only if every assert passes
node scripts/verify/kind-runtime.mjs --actor-kind claude
```

It installs into a throwaway prefix (never `$HOME`), replaces the generated `taskctl`
shim with a canned one — it records every argv — then drives the generated SessionStart
hook, asserting the **actual invocation** (exit code + argv + output), not just "it ran".
This is the step that covers the claim path in CI, where the real CLIs are absent. The full
matrix and the per-agent-kind coverage boundary (the bundle writes no hook for Pi; setup-meerkat's
optional worktree round can install a `tool_call` extension) are in
[`scheduling.md`](../scheduling.md#verifying-the-generated-hook).

## Isolated installs and testing

Nothing is ever written outside the destination paths in the table above. To install
without touching a real user home — for a test, or a CI assert — use a throwaway home.
The bundle agent kinds write their config, commands and trigger into the same home, so the whole
bundle stays isolated:

```bash
bash install.sh --target all --prefix /tmp/tp-home
MEERKAT_TASKPANEL_TARGET_HOME=/tmp/tp-home bash scripts/install/claude.sh   # env equivalent
```

The agent CLIs honour their own config-directory variables, so a fully isolated plugin
install is possible too:

```bash
HOME=/tmp/tp-home CLAUDE_CONFIG_DIR=/tmp/tp-home/.claude claude plugin marketplace add "$PWD"
HOME=/tmp/tp-home CODEX_HOME=/tmp/tp-home/.codex codex plugin marketplace add "$PWD"
```

## Uninstalling

Each agent kind's section above lists the plugin/bundle removal command. The skills themselves
are removed by deleting their destinations:

```bash
rm -rf ~/.claude/skills/{meerkat,meerkat-director,meerkat-chat,setup-meerkat,jobs,linus,turing,reviewer,worker}  # claude
rm -rf ~/.openclaw/skills/meerkat                                  # openclaw (no loop)
rm -rf ~/.codex/skills/{meerkat,meerkat-director,meerkat-chat,setup-meerkat,jobs,linus,turing,reviewer,worker}   # codex
rm -rf ~/.agents/skills/{meerkat,meerkat-director,meerkat-chat,setup-meerkat,jobs,linus,turing,reviewer,worker} # pi
```

Nothing is written outside those directories, and nothing is sent over the network — see
[`PRIVACY.md`](../../PRIVACY.md).

## Troubleshooting

- **`no 'node' on PATH` / `Node vNN.x is too old`** — Meerkat TaskPanel needs **Node >= 22**
  (the engine uses the built-in `node:sqlite` module). Install or upgrade Node and re-run;
  `--skip-node-check` bypasses the check if you know what you are doing.
- **`… left unchanged — use --force to overwrite` (or `already installed … unchanged`)** —
  that is the desired end state, not an error: a re-run leaves existing files and config
  keys untouched (merge, never clobber) and exits **0**. If you *meant* to change something
  (e.g. a new `--agent-name`), re-run with `--force` to overwrite the managed files/keys,
  or delete the destination first for a clean slate. See
  [Idempotence](#idempotence-re-running-and-force).
- **`skill source not found`** — run `install.sh` from this checkout (it resolves
  `skills/meerkat/` relative to itself); a `dist/` bundle must be unpacked first.
- **An older install still has `skills/meerkat-loop/`** — that skill is retired; the
  `meerkat-director` skill, the `meerkat-chat` console and the five role skills replace it. Nothing deletes the old
  directory for you (it may contain a roles tree someone edited), and detection reports it as
  legacy. Re-run the install, verify the new skills, then remove the old directory yourself.
- **OpenClaw does not list the plugin** — install the bundle at the repository root
  (`openclaw plugins install "$PWD" ...`); see the OpenClaw section above.
