# Install Wave Implementation Plan (init wizard, .mcpb path, setup skills)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Kill the #1 adoption blocker: `tachibot init` setup wizard, a distributed one-click Claude Desktop path (.mcpb), and three new skills (/setup, /spec, /triage) plus auto-fire-sharpened descriptions on four existing skills.

**Architecture:** The wizard is detection + emission first (pure, testable functions that detect keys/clients and emit exact per-client instructions), with a thin interactive layer using node:readline/promises — no new dependencies. It hooks into the existing bin via an argv check at the very top of server.ts (before any MCP/server init). Skills are markdown only. The .mcpb already builds via `npm run package:extension`; this wave makes it fresh and documented.

**Tech Stack:** TypeScript ESM, node built-ins only (readline/promises, fs, os, path), Jest, existing packaging script.

## Global Constraints

- Version stays **2.25.0** (committed but unpublished — this wave folds into that release; no new bump).
- No new npm dependencies.
- The argv hook must run BEFORE dotenv/server initialization side effects and must `process.exit(0)` after the wizard so `tachibot init` never starts an MCP server.
- Wizard NEVER writes API keys to disk without explicit per-file confirmation; it prefers EMITTING commands/config for the user to apply. It never prints key VALUES back (mask to first 6 chars).
- Skills frontmatter format: `name` / `description` / `user-invocable: true` (match existing skills exactly).
- CLAUDE.md + tools.config.json remain gitignored (disk edits only). `git add` ONLY named files. `npm test` must exit 0.
- No profile/schema/golden changes in this wave (no new MCP tools — `init` is a CLI path, not an MCP tool; the tool count stays 63 everywhere).

## File Structure

- Create: `src/cli/init.ts` — detection + emission pure functions AND the interactive runner
- Create: `test/cli/init.test.ts` — tests for the pure functions only
- Modify: `src/server.ts` (argv hook, ~3 lines at top of the entry flow)
- Create: `skills/setup/SKILL.md`, `skills/spec/SKILL.md`, `skills/triage/SKILL.md`
- Modify: `skills/review/SKILL.md`, `skills/redteam/SKILL.md`, `skills/jury/SKILL.md`, `skills/tachi/SKILL.md` (description lines only)
- Modify: `README.md` (Quick Start: wizard first, Desktop one-click section; skills table 14→17), CLAUDE.md (disk-only)

---

### Task 1: `tachibot init` wizard

**Files:**
- Create: `src/cli/init.ts`
- Test: `test/cli/init.test.ts`
- Modify: `src/server.ts` (top-of-entry argv hook)

**Interfaces:**
- Produces: `export interface DetectedSetup { keys: { name: string; envVar: string; present: boolean; unlocks: string }[]; clients: { claudeCode: boolean; claudeDesktop: boolean; desktopConfigPath: string | null } }`; `export function detectSetup(env?: NodeJS.ProcessEnv, probe?: { which: (bin: string) => boolean; exists: (p: string) => boolean }): DetectedSetup`; `export function buildClaudeCodeCommand(setup: DetectedSetup): string`; `export function buildDesktopSnippet(setup: DetectedSetup, profile: string): string`; `export async function runInitWizard(): Promise<void>`.
- Consumed by: server.ts argv hook.

- [ ] **Step 1: Write the failing test**

```typescript
// test/cli/init.test.ts
import { detectSetup, buildClaudeCodeCommand, buildDesktopSnippet } from "../../src/cli/init.js";

const probe = (haveClaude: boolean, haveDesktop: boolean) => ({
  which: (bin: string) => bin === "claude" && haveClaude,
  exists: (_p: string) => haveDesktop,
});

describe("tachibot init — detection and emission", () => {
  test("detects present and missing keys without leaking values", () => {
    const setup = detectSetup(
      { OPENROUTER_API_KEY: "sk-or-v1-secret123456", GOOGLE_API_KEY: "" } as any,
      probe(true, false),
    );
    const or = setup.keys.find((k) => k.envVar === "OPENROUTER_API_KEY")!;
    const gg = setup.keys.find((k) => k.envVar === "GOOGLE_API_KEY")!;
    expect(or.present).toBe(true);
    expect(gg.present).toBe(false);
    expect(JSON.stringify(setup)).not.toContain("secret123456");
  });

  test("detects clients via injected probe", () => {
    const setup = detectSetup({} as any, probe(true, true));
    expect(setup.clients.claudeCode).toBe(true);
    expect(setup.clients.claudeDesktop).toBe(true);
  });

  test("claude-code command uses the dual-bin-safe npx form and only present keys", () => {
    const setup = detectSetup({ PERPLEXITY_API_KEY: "pplx-abc" } as any, probe(true, false));
    const cmd = buildClaudeCodeCommand(setup);
    expect(cmd).toContain("claude mcp add tachibot");
    expect(cmd).toContain("npx -y -p tachibot-mcp tachibot");
    expect(cmd).toContain("--env PERPLEXITY_API_KEY=");
    expect(cmd).not.toContain("--env OPENROUTER_API_KEY=");
    expect(cmd).not.toContain("pplx-abc"); // placeholder, not the real value
  });

  test("desktop snippet is valid JSON with command tachibot and chosen profile", () => {
    const setup = detectSetup({ OPENROUTER_API_KEY: "x" } as any, probe(false, true));
    const snippet = buildDesktopSnippet(setup, "full");
    const parsed = JSON.parse(snippet);
    expect(parsed.mcpServers.tachibot.command).toBe("tachibot");
    expect(parsed.mcpServers.tachibot.env.TACHIBOT_PROFILE).toBe("full");
    expect(Object.keys(parsed.mcpServers.tachibot.env)).toContain("OPENROUTER_API_KEY");
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npm test -- test/cli/init`
Expected: FAIL — `Cannot find module '../../src/cli/init.js'`

- [ ] **Step 3: Write the implementation**

```typescript
// src/cli/init.ts
/**
 * `tachibot init` — setup wizard. Detection + emission first: pure functions
 * detect keys/clients and emit EXACT per-client instructions; a thin
 * readline layer only picks the client. Keys are never written to disk by
 * default and never echoed (masked to 6 chars). Node built-ins only.
 */
import { execSync } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import * as readline from "node:readline/promises";

const KEYS = [
  { name: "OpenRouter", envVar: "OPENROUTER_API_KEY", unlocks: "DeepSeek/GLM/Kimi/Qwen/MiniMax/StepFun/ERNIE + planner (~30 tools)" },
  { name: "Perplexity", envVar: "PERPLEXITY_API_KEY", unlocks: "web research tools" },
  { name: "Gemini / Google", envVar: "GOOGLE_API_KEY", unlocks: "Gemini tools + jury judge + diff_review/plan_critique" },
  { name: "OpenAI", envVar: "OPENAI_API_KEY", unlocks: "GPT-5.5 tools + spec_writer" },
  { name: "Grok / xAI", envVar: "GROK_API_KEY", unlocks: "Grok tools + debug_triage" },
] as const;

export interface DetectedSetup {
  keys: { name: string; envVar: string; present: boolean; unlocks: string }[];
  clients: { claudeCode: boolean; claudeDesktop: boolean; desktopConfigPath: string | null };
}

function defaultProbe() {
  return {
    which: (bin: string): boolean => {
      try { execSync(`command -v ${bin}`, { stdio: "ignore" }); return true; } catch { return false; }
    },
    exists: (p: string): boolean => fs.existsSync(p),
  };
}

function desktopConfigPath(): string {
  if (process.platform === "darwin") {
    return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
  }
  if (process.platform === "win32") {
    return path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
  }
  return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
}

export function detectSetup(
  env: NodeJS.ProcessEnv = process.env,
  probe = defaultProbe(),
): DetectedSetup {
  const keys = KEYS.map((k) => ({
    name: k.name,
    envVar: k.envVar,
    present: Boolean(env[k.envVar]?.trim()),
    unlocks: k.unlocks,
  }));
  // Gemini/Grok alternates count as present
  const alt = (primary: string, alternate: string) => {
    const row = keys.find((k) => k.envVar === primary)!;
    if (!row.present && env[alternate]?.trim()) row.present = true;
  };
  alt("GOOGLE_API_KEY", "GEMINI_API_KEY");
  alt("GROK_API_KEY", "XAI_API_KEY");

  const dcp = desktopConfigPath();
  return {
    keys,
    clients: {
      claudeCode: probe.which("claude"),
      claudeDesktop: probe.exists(dcp),
      desktopConfigPath: probe.exists(dcp) ? dcp : null,
    },
  };
}

export function buildClaudeCodeCommand(setup: DetectedSetup): string {
  const envFlags = setup.keys
    .filter((k) => k.present)
    .map((k) => `--env ${k.envVar}=<your-${k.name.toLowerCase().replace(/[^a-z]+/g, "-")}-key>`)
    .join(" \\\n  ");
  return [
    "claude mcp add tachibot \\",
    envFlags ? `  ${envFlags} \\` : null,
    "  -- npx -y -p tachibot-mcp tachibot",
  ].filter(Boolean).join("\n");
}

export function buildDesktopSnippet(setup: DetectedSetup, profile: string): string {
  const env: Record<string, string> = {};
  for (const k of setup.keys.filter((k) => k.present)) env[k.envVar] = `<your-${k.envVar}>`;
  env.TACHIBOT_PROFILE = profile;
  return JSON.stringify({ mcpServers: { tachibot: { command: "tachibot", env } } }, null, 2);
}

const mask = (v: string | undefined) => (v ? `${v.slice(0, 6)}…` : "");

export async function runInitWizard(): Promise<void> {
  const setup = detectSetup();
  const out = (s: string) => process.stdout.write(s + "\n");

  out("\nTACHIBOT INIT\n=============");
  out("\nAPI keys detected in this shell:");
  for (const k of setup.keys) {
    out(`  ${k.present ? "✓" : "✗"} ${k.name} (${k.envVar})${k.present ? ` ${mask(process.env[k.envVar])}` : ""} — ${k.unlocks}`);
  }
  if (!setup.keys.some((k) => k.present)) {
    out("\nNo keys found. Get ONE key to start — OPENROUTER_API_KEY unlocks the most tools (openrouter.ai).");
  }

  out("\nClients detected:");
  out(`  ${setup.clients.claudeCode ? "✓" : "✗"} Claude Code (claude on PATH)`);
  out(`  ${setup.clients.claudeDesktop ? "✓" : "✗"} Claude Desktop${setup.clients.desktopConfigPath ? ` (${setup.clients.desktopConfigPath})` : ""}`);

  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  try {
    const choice = (await rl.question("\nSet up for: [1] Claude Code  [2] Claude Desktop  [3] both  [q] quit > ")).trim();
    if (choice === "q") return;
    const profile = (await rl.question("Profile [full=all 63 tools | balanced | code_focus] (default: full) > ")).trim() || "full";

    if (choice === "1" || choice === "3") {
      out("\n— Claude Code — run this (fill in your real keys):\n");
      out(buildClaudeCodeCommand(setup));
      out("\nThen verify with /mcp inside Claude Code.");
    }
    if (choice === "2" || choice === "3") {
      out("\n— Claude Desktop — easiest: double-click the tachibot-mcp.mcpb extension package (see GitHub releases).");
      out("Or merge this into " + (setup.clients.desktopConfigPath ?? desktopConfigPath()) + ":\n");
      out(buildDesktopSnippet(setup, profile));
      out("\nThen restart Claude Desktop.");
    }
    out("\nFirst thing to run once connected: the `doctor` tool — it shows which tools your keys unlock.");
  } finally {
    rl.close();
  }
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `npm test -- test/cli/init`
Expected: PASS (4 tests)

- [ ] **Step 5: Wire the argv hook — THIN DISPATCHER (amended)**

AMENDED 2026-07-02 after implementation found the original inline-guard design unsound: ESM static imports are hoisted and evaluated before any top-level statement, so a textual `if (argv[2]==='init')` guard cannot prevent the side effects of server.ts's 34 static imports (profile logging, CustomWorkflowEngine construction, workflow YAML loads all fire first).

Correct design: make `src/server.ts` a thin dispatcher with ZERO static imports (a static import — including `export … from` re-exports — would re-trigger hoisting):

```typescript
#!/usr/bin/env node
// Thin bin dispatcher. ZERO static imports here — ESM hoists them before any
// guard runs, which would fire server-init side effects even for `init`.
if (process.argv[2] === "init") {
  const { runInitWizard } = await import("./cli/init.js");
  await runInitWizard();
  process.exit(0);
} else {
  await import("./server-main.js");
}
```

Move the entire previous server.ts body (imports and all) to a new `src/server-main.ts`, unchanged. Then find everything that imports from `server.js`/`server.ts` (grep test/ and src/ — the golden harness `test/golden/emit-schema.ts` is known to) and point those imports at `server-main.js` instead. The commit allowlist expands accordingly: `src/cli/init.ts test/cli/init.test.ts src/server.ts src/server-main.ts` plus any test-harness files whose imports had to be re-pointed (name them in the report).

- [ ] **Step 6: Verify end-to-end**

Run: `npm run build && printf 'q\n' | node dist/src/server.js init`
Expected: the wizard banner + key/client detection prints, then exits 0 WITHOUT starting the MCP server (no FastMCP startup logs).
Run: `npm test` — all suites pass, exit 0.

- [ ] **Step 7: Commit**

```bash
git add src/cli/init.ts test/cli/init.test.ts src/server.ts
git commit -m "feat(cli): tachibot init — setup wizard (key/client detection, per-client config emission)"
```

---

### Task 2: .mcpb one-click Desktop path

**Files:**
- Modify: `README.md` (Setup (Claude Desktop) section)
- Regenerate: `tachibot-mcp.mcpb` (repo-root artifact; check whether it's gitignored — `git check-ignore tachibot-mcp.mcpb`; the `*.mcpb` pattern IS in .gitignore, so the artifact ships via GitHub release assets, NOT the repo)

**Interfaces:**
- Consumes: `npm run package:extension` (existing script).
- Produces: fresh v2.25.0 .mcpb + README documentation. Release-asset attachment is USER-GATED (publishing) — emit the exact `gh` command for the user instead of running it.

- [ ] **Step 1:** Run `npm run package:extension`. Verify: exit 0 and `ls -la tachibot-mcp.mcpb` shows a fresh timestamp; `unzip -l tachibot-mcp.mcpb | head -20` shows a manifest.json + dist/ contents.
- [ ] **Step 2:** In README's "Setup (Claude Desktop)" section, add ABOVE the Gateway Mode JSON:

```markdown
**One-click (easiest):** download [`tachibot-mcp.mcpb`](https://github.com/byPawel/tachibot-mcp/releases/latest) from the latest release and double-click it — Claude Desktop installs the extension with no JSON editing. Add your API keys when prompted (or later via the extension settings).
```

- [ ] **Step 3:** Print (do NOT run) the attach command for the user, in your report: `gh release create v2.25.0 --title "..." --notes-file <notes> tachibot-mcp.mcpb` — release publishing needs the user's go-ahead.
- [ ] **Step 4:** `npm test` exit 0; commit:

```bash
git add README.md
git commit -m "docs: one-click Claude Desktop install via .mcpb release asset"
```

---

### Task 3: /setup, /spec, /triage skills + auto-fire descriptions

**Files:**
- Create: `skills/setup/SKILL.md`, `skills/spec/SKILL.md`, `skills/triage/SKILL.md`
- Modify: description frontmatter ONLY in `skills/review/SKILL.md`, `skills/redteam/SKILL.md`, `skills/jury/SKILL.md`, `skills/tachi/SKILL.md`

**Interfaces:**
- Consumes: `doctor`, `spec_writer` (params: request req, context?, files?, format?), `debug_triage` (params: error req, code?, files?, context?, runtime?), `planner_maker`.
- Produces: 17 skills total; install-skills.sh auto-derives (no script change).

- [ ] **Step 1: `skills/setup/SKILL.md`** (match existing frontmatter format exactly — name/description/user-invocable):

```markdown
---
name: setup
description: Use when TachiBot tools seem missing, a provider isn't working, or the user asks to set up/configure TachiBot or its API keys — runs doctor and walks through fixing the gaps
user-invocable: true
---

# /setup — Guided TachiBot Configuration

1. Call the `doctor` tool. Relay: which keys are detected, how many tools are visible vs hidden, and the active profile.
2. For each MISSING key the user wants, tell them exactly where to get it (OpenRouter: openrouter.ai/keys — unlocks the most tools; Perplexity: perplexity.ai; Google AI Studio; OpenAI platform; xAI console) and how to add it for their client: Claude Code → `claude mcp add` with `--env` flags (or edit the tachibot entry in ~/.claude.json); Claude Desktop → extension settings or claude_desktop_config.json env block.
3. If they want fewer tools/tokens, explain profiles: full (63, default), balanced (52), code_focus (41), minimal (13) — set via TACHIBOT_PROFILE env in the same config.
4. After they update config, remind them to restart the client (or /mcp reconnect in Claude Code), then re-run `doctor` to confirm the delta.
5. Suggest a first call that exercises their newest key (e.g. jury for Gemini+any, grok_search for Grok).
```

- [ ] **Step 2: `skills/spec/SKILL.md`**:

```markdown
---
name: spec
description: Use when a feature request is loose or ambiguous and needs a reviewable spec before planning — turns "add X somehow" into user stories, acceptance criteria, out-of-scope, and open questions via spec_writer
user-invocable: true
---

# /spec — Request → Reviewable Spec

1. Collect the request verbatim (do not pre-polish it — ambiguity is input, spec_writer preserves it as open questions).
2. Gather context: what exists today (read the relevant code if pointed at it), constraints, user base. Pass file paths via `files`.
3. Call `spec_writer` with `request`, `context`, `files`, and `format` if the user prefers user_story or gherkin (default both).
4. Relay the spec. Lead with the OPEN QUESTIONS — those are the decisions the user must make; the rest is for review.
5. Once the user answers/edits, offer the next step: feed the approved spec to `planner_maker` (or /blueprint) to plan the HOW.

Requires OPENAI_API_KEY. If the tool returns its missing-key error, relay it and suggest /setup.
```

- [ ] **Step 3: `skills/triage/SKILL.md`**:

```markdown
---
name: triage
description: Use when the user hits an error, exception, or stack trace and the cause isn't obvious — returns RANKED root-cause hypotheses with the cheapest discriminating check for each, via debug_triage
user-invocable: true
---

# /triage — Ranked Bug Triage

1. Collect the error/stack trace verbatim into `error`. Never trim the trace.
2. Gather cheap context: repro steps and recent changes (`context`), runtime/versions (`runtime`), and the implicated source (`files` with line ranges around the frames, e.g. 'src/app.ts:30-60').
3. Call `debug_triage`.
4. Relay the ranked hypotheses with their likelihoods, then run (or offer to run) the TOP hypothesis's discriminating check — logs, a breakpoint, a one-liner — before touching any fix.
5. If the check kills the top hypothesis, promote the named runner-up and check that one; only implement the minimal fix once a hypothesis is CONFIRMED. Then add the locking test the tool suggested.

Requires GROK_API_KEY (or XAI_API_KEY). If the tool returns its missing-key error, relay it and suggest /setup.
```

- [ ] **Step 4: Sharpen auto-fire descriptions** (frontmatter `description:` line ONLY; do not touch skill bodies):
- `skills/review/SKILL.md` → `description: Use when code changes are ready for review — a diff exists, the user says "review my changes/PR", or a commit is about to happen. Multi-model diff review (Kimi, DeepSeek, GPT-5.5 panel + Gemini judge) with a MERGEABLE verdict`
- `skills/redteam/SKILL.md` → `description: Use when the user shares or finishes a plan and wants it stress-tested before execution — adversarial multi-model pre-mortem with ranked risks, mitigations, and concrete plan edits`
- `skills/jury/SKILL.md` → `description: Use for any quick A-vs-B decision or "is this right?" judgment worth a second opinion — parallel lab-diverse jurors + Gemini synthesis in one call, <15s`
- `skills/tachi/SKILL.md` → `description: Use when the user asks what TachiBot can do, which tools/skills exist, or which API keys are configured — help and discovery entry point`

- [ ] **Step 5: Verify** — `HOME=$(mktemp -d) bash scripts/install-skills.sh` advertises 17 skills including /setup, /spec, /triage; `npm test` exit 0.

- [ ] **Step 6: Commit**

```bash
git add skills/setup/SKILL.md skills/spec/SKILL.md skills/triage/SKILL.md skills/review/SKILL.md skills/redteam/SKILL.md skills/jury/SKILL.md skills/tachi/SKILL.md
git commit -m "feat(skills): /setup, /spec, /triage + auto-fire descriptions for review/redteam/jury/tachi (17 skills)"
```

---

### Task 4: Docs settlement (17 skills + wizard-first Quick Start)

**Files:**
- Modify: `README.md`, CLAUDE.md (disk-only)

- [ ] **Step 1: README.md** — skills intro "14 slash commands"→"17"; add three table rows (style-matched): `/setup` (guided configuration), `/spec` (request → reviewable spec), `/triage` (ranked bug triage). In Quick Start, add ABOVE the Claude Code one-liner: a "Setup wizard" block: ```npx -y -p tachibot-mcp tachibot init``` with one line: "detects your keys and clients, prints the exact config for Claude Code and Claude Desktop."
- [ ] **Step 2: CLAUDE.md (disk-only)** — skills table: add the three new rows; note count 17.
- [ ] **Step 3: Verify** — `npm run build && npm test` exit 0; grep README for "14 slash" → none.
- [ ] **Step 4: Commit**

```bash
git add README.md
git commit -m "docs: wizard-first Quick Start; 17 skills"
```

---

## Self-Review Notes

- Spec coverage: goal items — wizard (Task 1), easier Desktop path (Task 2), install skill + missing skills (Task 3), docs (Task 4). Context-viewer question was answered outside the plan (not buildable via MCP — client-side).
- Type consistency: `DetectedSetup`/`detectSetup`/`buildClaudeCodeCommand`/`buildDesktopSnippet` names match between test and implementation; the npx form matches the dual-bin-safe command README already uses.
- Judgment calls: wizard emits config rather than writing it (safety + testability); `*.mcpb` is gitignored so distribution is via release assets (publishing = user-gated); no new MCP tool for init (CLI path — keeps tool count/golden stable).
