setup-catalyst · git:20260824.3b80357 · 2026-08-24 · sha256 9e83a934f0c530ea
setup-catalyst git:20260824.3b80357B
Immutable. This exact content is served forever at /api/v1/blob/9e83a934f0c530ea.
---
name: setup-catalyst
description: "Diagnose and fix Catalyst setup issues. Validates tools, database, config, OTel, direnv, and thoughts. Automatically fixes what it can — creates directories, initializes the database, sets WAL mode, runs migrations. Use for new installs, upgrades, or when something isn't working."
---
# Setup Catalyst
Diagnose the full Catalyst environment, fix everything fixable, and verify the fixes worked.
## Phase 1: Diagnose
Locate and run the health check script:
```bash
# Backing scripts live in catalyst-dev (the shared framework core). Resolve them
# (and fail fast with a clear message if catalyst-dev is not installed).
source "${CLAUDE_PLUGIN_ROOT:-plugins/foundry}/scripts/require-catalyst-dev.sh" \
"${CLAUDE_PLUGIN_ROOT:-plugins/foundry}" || exit 1
SCRIPT="${CATALYST_DEV_SCRIPTS}/check-setup.sh"
bash "$SCRIPT" 2>&1 || true
```
Parse the output. Categorize every warning and failure into:
- **Auto-fixable** — can be fixed right now without user input
- **Needs user input** — requires API tokens, credentials, or decisions
- **Manual only** — requires tool installation or external action
## Phase 2: Fix
For auto-fixable issues, fix them immediately — don't ask, just do it. These are safe, local,
reversible operations.
**Exception: thoughts/ repair is NOT a bare `mkdir`.** The humanlayer thoughts system expects
`thoughts/shared` and `thoughts/global` to be **symlinks** into a central thoughts repo. A bare
`mkdir` over a clobbered symlink silently routes all subsequent agent writes to a non-syncing
local directory. Always route thoughts repair through `catalyst-thoughts.sh`, and treat a
regular-directory-where-a-symlink-should-be as **fatal** — surface the recovery command to the
user rather than overwriting anything.
| Issue | Fix |
|-------|-----|
| `~/catalyst/` missing | `mkdir -p ~/catalyst/{wt,events,history}` |
| `~/catalyst/wt/` missing | `mkdir -p ~/catalyst/wt` |
| `~/catalyst/events/` missing | `mkdir -p ~/catalyst/events` |
| Database missing or schema incomplete | Run `catalyst-db.sh init` (locating it the same way as the check script) |
| `schema_migrations` table missing | Run `catalyst-db.sh init` — it's idempotent |
| WAL mode not set | `sqlite3 ~/catalyst/catalyst.db 'PRAGMA journal_mode=WAL;'` |
| `thoughts/shared/<dir>` missing | Run `bash plugins/dev/scripts/catalyst-thoughts.sh init-or-repair` (re-uses humanlayer when configured; warns loudly when no thoughts repo is set up) |
| `~/.catalyst/bin/` missing OR any catalyst-* symlink absent/broken | Run `bash plugins/dev/scripts/install-cli.sh` — idempotent, safe to re-run. If `$HOME/.catalyst/bin` is not on `$PATH`, the script prints the exact line to add to `~/.zshrc` or `~/.bashrc` — relay that to the user so they can finish the one-time PATH setup. |
| `thoughts/shared` is a regular directory (not a symlink) | **Fatal — do not auto-fix.** Tell the user the humanlayer symlink was clobbered and show recovery: `mv thoughts/shared thoughts/shared.orphaned-$(date +%Y%m%d)` then `bash plugins/dev/scripts/catalyst-thoughts.sh init-or-repair` |
| Drift detected: keys present in `plugins/dev/templates/config.template.json` but missing from `.catalyst/config.json` (CTL-489) | Enumerate via `bash plugins/dev/scripts/check-config-drift.sh --json`. Generate a preview merge to a temp file via `--merge-into /tmp/merged.json` and show the user `diff -u .catalyst/config.json /tmp/merged.json`. On user confirmation, `jq` deep-merge into the real file: `bash plugins/dev/scripts/check-config-drift.sh --merge-into .catalyst/config.json.new && mv .catalyst/config.json.new .catalyst/config.json`. Merge preserves every existing user value (project on the right of jq's `*` recursive merge). |
| Profile drift between `.catalyst/config.json` and humanlayer mapping | Run `bash plugins/dev/scripts/catalyst-thoughts.sh init-or-repair` — it now auto-repairs drift by running `humanlayer thoughts uninit --force && humanlayer thoughts init --profile <config profile> --directory <config directory>`. (Plain `humanlayer thoughts init --force` does NOT update an existing repo→profile mapping, so the `uninit` step is required.) |
| Agent house-rules block (the "Working the Loop" reflexes) — run this **proactively every setup**, not only when check-project-setup.sh §9 flags it (§9 is skipped for a repo with no agent doc at all, so a doc-less repo would otherwise never be seeded) | From the project root run `bash "${CATALYST_DEV_SCRIPTS}/ensure-agent-house-rules.sh" --fix` (resolve `${CATALYST_DEV_SCRIPTS}` via the Phase-1 `require-catalyst-dev.sh` source — the plugin lives outside the consumer repo, so a repo-relative path won't exist). Idempotent and self-detecting: seeds the canonical block into the doc the driving agent loads (AGENTS.md when CLAUDE.md imports it, else CLAUDE.md), creates an `AGENTS.md` + thin `@AGENTS.md` bridge if the repo has neither, migrates a legacy block to sentinels, and re-syncs a stale one — a no-op when already current. Safe, local, reversible — this is the auto-seed that makes every enrolled repo carry the reflexes, so run it without asking. |
| Single-harness repo (missing `@AGENTS.md` bridge / non-portable skills) — run this **proactively every setup**, not only when check-project-setup.sh §10 flags it (§10 is a hot-path, TTL-gated warning, and a direct `catalyst-foundry:setup-catalyst` invocation for an upgrade never runs check-project-setup.sh at all, so relying on the §10 flag alone would silently skip the migration) | From the project root dry-run the classifier: `bash "${CATALYST_DEV_SCRIPTS}/migrate-dual-harness.sh" --repo . --quiet`, then act on its exit code **per the rc contract owned by the `catalyst-foundry:migrate-dual-harness` skill** (single source of truth — read that skill's Phase 2 table; do not rely on a copy here). In brief: rc `0` no-op; rc `10` apply `--fix` (idempotent, safe without asking); rc `11` **stop** and tell the user to run `/catalyst-foundry:migrate-dual-harness` (setup cannot auto-invoke it); rc `4` surface the stderr diagnostic verbatim and stop. Any drift between this summary and the owning skill: the owning skill wins. |
**Config-template drift (CTL-489).** When the template gains a key that an existing project's
`.catalyst/config.json` lacks (the original CTL-487 silent-fallback bug — catalyst itself ran in
`oneshot-legacy` for two months because `orchestration.dispatchMode` was absent), Phase 2 surfaces
a unified diff and asks for confirmation before merging. Concretely:
1. Run `bash plugins/dev/scripts/check-config-drift.sh --json --config .catalyst/config.json
--template <template>` to enumerate missing leaves. The template path comes from
`$CLAUDE_PLUGIN_ROOT/templates/config.template.json` in production, or
`plugins/dev/templates/config.template.json` when dogfooding from the repo.
2. If the array is non-empty, generate a preview merge file via
`bash plugins/dev/scripts/check-config-drift.sh --merge-into /tmp/merged.json` and show the user
the unified diff: `diff -u .catalyst/config.json /tmp/merged.json`.
3. Ask: "Apply these template additions? [y/N]". Default is NO — drift is non-fatal and the
warning will keep showing on subsequent workflow invocations until the user opts in.
4. On confirmation, write the merged file atomically to `.catalyst/config.json` (`mv` from a
sibling `.tmp` file). The merge uses jq's `*` operator with the project on the right —
existing values always win, missing keys are added.
5. The merge **never** overwrites existing user values. If the user has a custom
`catalyst.filter.groqModel`, the template's default is NOT applied to that key.
6. If the user declines, leave `.catalyst/config.json` untouched. The drift warning continues to
appear on subsequent workflow invocations, providing passive nagging until resolved.
**Linear bot user ID (CTL-550 / CTL-749 / CTL-549).** `catalyst.monitor.linear.botUserId` is the
Linear user UUID of the Catalyst app-actor — the "Linear for Agents" app identity that posts
comments *as the app* (installed by CTL-550). It is **required for the Linear app-actor comms
channel** — i.e. when the execution-core daemon mirrors phase-agent output to Linear and wakes on
human replies (CTL-550 / CTL-549 / CTL-749). It is the self-echo / loop-prevention guard for the
whole bidirectional channel. CTL-749 / CTL-549 built a channel where a human reply on a ticket
wakes a parked worker; without `botUserId` loaded, the system cannot tell the agent's *own*
comments and description-updates apart from a human's, so (a) the agent's own mirror comments get
written into the worker `inbox.jsonl` as if they were human input (noise / false "human replied"
signals), and (b) bot-authored issue events feed back into the event log as write loops. The
orch-monitor's Linear webhook handler suppresses bot-authored issue events using this value, and
the execution-core daemon uses it to filter the agent's self-echo from each worker's inbox.
This value is **workspace-specific** and is NOT shipped in the committed template
(`config.template.json` keeps it `null`). It is not secret — it appears on every comment the app
posts — but it must be obtained per workspace and written into Layer 1
`.catalyst/config.json → catalyst.monitor.linear.botUserId` (alongside the other `monitor.linear`
keys such as `teams` and `webhookSecretEnv`). To obtain it, query `viewer.id` with the app-actor
token (the app OAuth credentials live in Layer 2
`~/.config/catalyst/config-<projectKey>.json → catalyst.linear.agent.{clientId,clientSecret,accessToken}`):
```bash
TOKEN=$(jq -r '.catalyst.linear.agent.accessToken' ~/.config/catalyst/config-<projectKey>.json)
BOT_ID=$(curl -s -X POST https://api.linear.app/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"query{viewer{id name}}"}' | jq -r .data.viewer.id)
```
(Alternatively, mint a fresh app token via `grant_type=client_credentials` with `actor=app` and
`scope="app:mentionable,app:assignable"` at `POST https://api.linear.app/oauth/token`, then run the
same `viewer{id}` query.) Write `$BOT_ID` into
`.catalyst/config.json → catalyst.monitor.linear.botUserId`. Both the monitor and the daemon read
`botUserId` **only at startup**, so after setting it you must restart them:
`catalyst-monitor stop && catalyst-monitor start`, then `catalyst-execution-core restart`. This is
a needs-user-input step (it requires the app-actor credentials), not an auto-fix — never write the
value for the user without confirming it came from their own app-actor token.
**Non-interactive / headless mode (CTL-842).** `setup-catalyst.sh` can run without a
controlling terminal — CI pipelines, SSH exec, cron, and `curl | bash` via `curl … | bash -s --
--non-interactive`. Three flags activate the mode:
- `--non-interactive` or `--defaults` CLI flags (both identical)
- `CATALYST_AUTONOMOUS=1` environment variable (set by execution-core workers)
In this mode:
- `ask_yes_no` returns the default answer without reading stdin; a 3rd argument overrides the NI
answer (install-offer sites pass `"n"` so they silently decline in CI).
- `prompt_value` echoes the default to stderr and returns it without consuming stdin.
- Install offers (`npm install -g humanlayer`, `brew install jq`, etc.) are declined automatically.
- The Linear config step is skipped when no token is discoverable from the environment or standard
config paths (`~/.config/catalyst/`, `~/.config/humanlayer/`). Prints: `Skipping Linear
(non-interactive, no token discoverable)`.
- The Sentry, PostHog, and Exa config steps are skipped unconditionally in NI mode.
- When multiple Linear teams or Sentry orgs/projects are found, the first entry is auto-selected
with a printed notice — no prompt.
The tty redirect (`exec </dev/tty`) runs only in interactive mode and only when `can_open_tty`
confirms the device is actually openable (subshell probe: `(: </dev/tty) 2>/dev/null`). This
prevents ENXIO crashes on PTY-less CI runners where `/dev/tty` exists as a device node but is
not attached to a session.
The source guard (`if ! (return 0 2>/dev/null); then main "$@"; fi`) replaces the old
`BASH_SOURCE[0]`-based check and works correctly in `curl | bash` pipelines where
`BASH_SOURCE[0]` is unset.
**Execution-core state contract (CTL-564).** `setup-catalyst.sh` runs an extra
step — `setup_execution_core_states` — right after the Linear workflow-state
fetch for every `--full` repo (CTL-722: not gated on `dispatchMode` any more).
That step delegates to the standalone
`plugins/dev/scripts/setup-execution-core-states.sh`, which ensures the team's
contract workflow states exist (`Todo` + `Research`, `Plan`, `Implement`,
`Validate`, `PR`; `Triage` already exists), writes the 10-phase → 5-state
collapse `stateMap` (idempotent — preserves user-customised maps), refreshes
`stateIds`, and upserts the team's entry in the central
`~/catalyst/execution-core/registry.json`. A Linear-permission failure in
the standalone script never aborts setup. The standalone script is also
idempotent and can be run directly per team (`setup-execution-core-states.sh
--config .catalyst/config.json [--dry-run] [--json]`).
**Linear git automations (CTL-759).** As its last Linear step,
`setup-execution-core-states.sh` reconciles the team's *git automations* —
Linear's built-in "move ticket on git event" rules. It pins exactly two
(`start` → `PR`, `merge` → `Done`) and deletes any `review` automation, so the
execution-core daemon stays the single authority on ticket state. The reconcile
is best-effort and tolerant: a Linear permission/transport failure prints a
WARNING and continues — it never aborts setup and never alters the script's exit
codes. `check-project-setup.sh` (hot path) warns on drift via a TTL-gated cached
read; a missing per-project token is a silent skip. Separately, Linear's
**branch-name "magic words" toggle** (Settings → Team → Workflow → Git) has no
API surface and **cannot** be reconciled — it must be turned OFF by hand, or it
races the daemon and re-introduces the CTL-758 backward state-write.
**Execution-core daemon env / proxy audit (machine-local, opt-in).**
`catalyst-execution-core start` sources a machine-local env file —
`~/.config/catalyst/execution-core.env` (override with `CATALYST_EXECUTION_CORE_ENV`)
— right before it launches the daemon, so every var it exports is inherited by the
daemon and every phase-agent bg job. An **absent file is a complete no-op** and is
the common case; this is **not** auto-fixable because the values (proxy port, MITM
CA path) are machine-specific. The committed template
`plugins/dev/templates/execution-core.env.example` documents every option. The two
uses are (1) routing the daemon's Linear/gh fetch traffic through a local mitmproxy
audit and (2) widening the Linear state-cache TTL (`LINEAR_STATE_CACHE_TTL_MS`).
The risk this guards against: a proxy that is configured but quietly broken silently
kills the daemon's Linear connectivity on a fresh or changed machine, with nothing
obvious to debug. So when the env file sets a proxy, `check-setup.sh` verifies it and
warns **loudly + actionably** on each failure mode — (a) the proxy port is not
listening, (b) `NODE_EXTRA_CA_CERTS` points at a missing file, or (c) `NODE_USE_ENV_PROXY=1`
is missing. The `NODE_USE_ENV_PROXY` flag matters because Node 20+/24+ native fetch
(undici) **ignores** `HTTPS_PROXY`/`HTTP_PROXY` without it — so the daemon's calls
would bypass the audit entirely while looking perfectly healthy. `check-project-setup.sh`
(the hot-path gate) carries only that one silent-bypass warning; full port/CA
diagnostics live in `check-setup.sh`. Treat any of these as **needs-user-input** — relay
the specific warning and fix, never write a machine path on the user's behalf, and
remind them to `catalyst-execution-core restart` after editing (the daemon re-sources
the file only on start/restart).
For issues needing user input, explain what's needed and how to provide it:
| Issue | What to tell the user |
|-------|----------------------|
| Linear API token not set | Show the secrets file path, explain where to get the token from Linear settings |
| No project config | Suggest running `setup-catalyst.sh` or offer to create a minimal `.catalyst/config.json` interactively |
| direnv not installed | Show `brew install direnv` and the shell hook setup |
| Linear "magic words" auto-move ON | Tell the user to turn it OFF in Settings → Team → Workflow → Git — it races the execution-core daemon and causes backward state writes (CTL-758). No API surface; must be toggled by hand. |
| Linear `review` git automation set | Run `setup-execution-core-states.sh` to remove it; the pipeline owns the Validate/review state, not Linear. |
| Personal git automations override team ones | Remind the user that Linear lets each member set *personal* git automations that shadow the team defaults — check Settings → Account → Git if drift persists after the team reconcile. |
| Proxy audit / daemon env wanted | Copy `plugins/dev/templates/execution-core.env.example` to `~/.config/catalyst/execution-core.env`, uncomment the vars you need (proxy + CA + `NODE_USE_ENV_PROXY=1`, and/or `LINEAR_STATE_CACHE_TTL_MS`), then `catalyst-execution-core restart`. **Needs user input, not auto-fix** — the proxy port and CA path are machine-specific. Never write machine paths for the user. |
| Daemon env proxy configured but broken | The check reports the exact failure: port not listening (start `mitmdump … --listen-port <port>` or unset the proxy), `NODE_EXTRA_CA_CERTS` missing (fix the path / re-run mitmproxy to regenerate its CA), or `NODE_USE_ENV_PROXY=1` missing (add it — without it Node fetch silently bypasses the audit). Relay the specific warning + fix; restart the daemon after. |
**Observability (OTel) is optional.** If Docker or OTel containers aren't found, note it as
informational — don't treat it as an issue. Point the user to
https://github.com/ryanrozich/claude-code-otel if they want to set it up.
## Phase 3: Verify
After fixing, run the health check script again (re-resolve, since each bash block
runs in a fresh shell):
```bash
source "${CLAUDE_PLUGIN_ROOT:-plugins/foundry}/scripts/require-catalyst-dev.sh" \
"${CLAUDE_PLUGIN_ROOT:-plugins/foundry}" || exit 1
bash "${CATALYST_DEV_SCRIPTS}/check-setup.sh" 2>&1 || true
```
Re-run the drift check independently to confirm zero remaining drift (CTL-489):
```bash
bash plugins/dev/scripts/check-config-drift.sh \
--config .catalyst/config.json \
--template plugins/dev/templates/config.template.json
```
Compare the before/after results. Report:
1. What was fixed (with counts)
2. What still needs attention (with specific next steps)
3. Overall status
## Output Format
```
── Catalyst Setup ──────────────────────────────
[Phase 1 output from check-setup.sh]
── Fixing Issues ───────────────────────────────
✅ Created ~/catalyst/events/
✅ Initialized session database
✅ Set WAL mode
✅ Created thoughts/shared/reports/
── Config Drift ────────────────────────────────
Detected 2 missing template keys:
• catalyst.orchestration.dispatchMode → "phase-agents"
• catalyst.filter.groqModel → "llama-3.1-8b-instant"
Preview diff:
--- .catalyst/config.json
+++ /tmp/merged.json
+ "orchestration": { "dispatchMode": "phase-agents" },
+ "filter": { "groqModel": "llama-3.1-8b-instant" },
Apply these template additions? [y/N] y
✅ Merged 2 keys into .catalyst/config.json (existing values preserved)
── Verification ────────────────────────────────
[Phase 3 output from check-setup.sh]
── Summary ─────────────────────────────────────
Fixed 4 issues automatically.
Still needs attention:
• Linear API token — add to ~/.config/catalyst/config-<project>.json
• OTel stack — run: docker compose up -d
```
## Important
- **Always run the check script first** — don't guess what's wrong
- **Fix silently** — auto-fixable issues are safe operations, don't ask permission for mkdir or
sqlite3 pragmas
- **Always verify after fixing** — run the check script a second time to confirm
- **Never touch secrets** — don't write API tokens or credentials, just tell the user where to put
them
- **Idempotent** — safe to run multiple times, won't break anything that's already working