# CodeWiki Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Auto-generate comprehensive wiki documentation from Java codebases using hierarchical module decomposition and multi-agent synthesis.

**Architecture:** Phase 1 builds the knowledge graph and decomposes it into a module tree (pure static analysis). Phases 2-3 use Claude Agent SDK subagents to generate leaf-module docs bottom-up, then synthesize parent docs, then a root overview. The wiki is served as markdown files from `.codewiki/` and displayed in a new Wiki tab alongside Chat and Code.

**Tech Stack:** Next.js 15, Claude Agent SDK (subagents), tree-sitter, @msgpack/msgpack, React, TailwindCSS

---

### Task 1: Module Tree Builder

**Files:**
- Create: `src/lib/repopilot/module-tree.ts`

**Context:**
- Read `src/lib/repopilot/types.ts` for `NodeType`, `EdgeType`, `NodeId`
- Read `src/lib/repopilot/graph/knowledge-graph.ts` for `KnowledgeGraph` API: `getNodesByType()`, `getNodeAttrs()`, `getNeighbors()`
- The knowledge graph has `PACKAGE` nodes with `name` attribute (e.g., `"jp.co.kopt.koptBp.cbs"`)
- Each package has `CONTAINS` edges to `CLASS`/`INTERFACE`/`ENUM` nodes
- Each class has `CONTAINS` edges to `METHOD`/`FIELD`/`CONSTRUCTOR` nodes
- Node attributes include `file_path`, `name`, `type`

**Step 1: Create the module tree types and builder**

```typescript
// src/lib/repopilot/module-tree.ts
import type { KnowledgeGraph } from "./graph/knowledge-graph";
import { NodeType, EdgeType } from "./types";

export interface ModuleNode {
  id: string;
  name: string;
  path: string;
  level: "root" | "package" | "subpackage" | "class_group";
  files: string[];
  classes: string[];
  children: ModuleNode[];
  parent?: string;
  docPath?: string;
  status: "pending" | "generating" | "done";
}

/**
 * Build a hierarchical module tree from the knowledge graph.
 *
 * Strategy:
 * 1. Get all PACKAGE nodes, group by top-level package prefix
 * 2. For each package, collect classes via CONTAINS edges
 * 3. Build hierarchy from package naming (split on ".")
 * 4. If a leaf has >15 classes, split into class_group by naming pattern
 */
export function buildModuleTree(graph: KnowledgeGraph): ModuleNode {
  const packageIds = graph.getNodesByType(NodeType.PACKAGE);

  // Collect package info: { packageName → { files, classes } }
  const packageMap = new Map<string, { files: Set<string>; classes: string[] }>();

  for (const pkgId of packageIds) {
    const attrs = graph.getNodeAttrs(pkgId);
    const pkgName = attrs.name as string;
    if (!pkgName) continue;

    const entry = packageMap.get(pkgName) ?? { files: new Set<string>(), classes: [] };

    // Get classes contained in this package
    const children = graph.getNeighbors(pkgId, EdgeType.CONTAINS, "out");
    for (const childId of children) {
      try {
        const childAttrs = graph.getNodeAttrs(childId);
        const childType = childAttrs.type as string;
        if (["class", "interface", "enum"].includes(childType)) {
          entry.classes.push(childAttrs.name as string);
          if (childAttrs.file_path) {
            entry.files.add(childAttrs.file_path as string);
          }
        }
      } catch {
        // node not found
      }
    }

    packageMap.set(pkgName, entry);
  }

  // Find common prefix to strip (e.g., "jp.co.kopt.")
  const allPkgNames = [...packageMap.keys()];
  const commonPrefix = findCommonPrefix(allPkgNames);

  // Build tree from stripped package names
  const root: ModuleNode = {
    id: "root",
    name: "root",
    path: "",
    level: "root",
    files: [],
    classes: [],
    children: [],
    status: "pending",
  };

  // Group packages by top-level name after stripping prefix
  const topLevelGroups = new Map<string, string[]>();
  for (const pkgName of allPkgNames) {
    const stripped = pkgName.startsWith(commonPrefix)
      ? pkgName.slice(commonPrefix.length)
      : pkgName;
    const parts = stripped.split(".");
    const topLevel = parts[0];
    const existing = topLevelGroups.get(topLevel) ?? [];
    existing.push(pkgName);
    topLevelGroups.set(topLevel, existing);
  }

  // Create top-level package modules
  for (const [topName, pkgNames] of topLevelGroups) {
    const topModule: ModuleNode = {
      id: topName,
      name: topName,
      path: topName,
      level: "package",
      files: [],
      classes: [],
      children: [],
      parent: "root",
      docPath: `${topName}.md`,
      status: "pending",
    };

    // Group sub-packages
    const subGroups = new Map<string, string[]>();
    for (const pkgName of pkgNames) {
      const stripped = pkgName.startsWith(commonPrefix)
        ? pkgName.slice(commonPrefix.length)
        : pkgName;
      const parts = stripped.split(".");
      if (parts.length > 1) {
        const subName = parts.slice(1).join(".");
        const existing = subGroups.get(subName) ?? [];
        existing.push(pkgName);
        subGroups.set(subName, existing);
      } else {
        // Classes directly in the top-level package
        const info = packageMap.get(pkgName);
        if (info) {
          topModule.files.push(...info.files);
          topModule.classes.push(...info.classes);
        }
      }
    }

    if (subGroups.size > 0) {
      for (const [subName, subPkgNames] of subGroups) {
        const allFiles: string[] = [];
        const allClasses: string[] = [];
        for (const pn of subPkgNames) {
          const info = packageMap.get(pn);
          if (info) {
            allFiles.push(...info.files);
            allClasses.push(...info.classes);
          }
        }

        const subModule: ModuleNode = {
          id: `${topName}.${subName}`,
          name: subName,
          path: `${topName}/${subName}`,
          level: "subpackage",
          files: [...new Set(allFiles)],
          classes: allClasses,
          children: [],
          parent: topName,
          docPath: `${topName}/${subName}.md`,
          status: "pending",
        };

        // Split large modules into class groups
        if (allClasses.length > 15) {
          const groups = groupClassesByPattern(allClasses, allFiles);
          for (const [groupName, group] of groups) {
            subModule.children.push({
              id: `${topName}.${subName}.${groupName}`,
              name: groupName,
              path: `${topName}/${subName}/${groupName}`,
              level: "class_group",
              files: group.files,
              classes: group.classes,
              children: [],
              parent: subModule.id,
              docPath: `${topName}/${subName}/${groupName}.md`,
              status: "pending",
            });
          }
          // Clear parent's direct files/classes since they're in children
          subModule.files = [];
          subModule.classes = [];
        }

        topModule.children.push(subModule);
      }
    }

    // If no sub-packages but >15 classes, split top-level too
    if (topModule.children.length === 0 && topModule.classes.length > 15) {
      const groups = groupClassesByPattern(topModule.classes, topModule.files);
      for (const [groupName, group] of groups) {
        topModule.children.push({
          id: `${topName}.${groupName}`,
          name: groupName,
          path: `${topName}/${groupName}`,
          level: "class_group",
          files: group.files,
          classes: group.classes,
          children: [],
          parent: topName,
          docPath: `${topName}/${groupName}.md`,
          status: "pending",
        });
      }
      if (topModule.children.length > 0) {
        topModule.files = [];
        topModule.classes = [];
      }
    }

    root.children.push(topModule);
  }

  return root;
}

function findCommonPrefix(names: string[]): string {
  if (names.length === 0) return "";
  const parts = names.map((n) => n.split("."));
  const common: string[] = [];
  for (let i = 0; i < parts[0].length; i++) {
    const seg = parts[0][i];
    if (parts.every((p) => p[i] === seg)) {
      common.push(seg);
    } else {
      break;
    }
  }
  // Keep at least the last segment for meaningful names
  if (common.length > 0 && common.length === parts[0].length) {
    common.pop();
  }
  return common.length > 0 ? common.join(".") + "." : "";
}

function groupClassesByPattern(
  classes: string[],
  files: string[],
): Map<string, { classes: string[]; files: string[] }> {
  const groups = new Map<string, { classes: string[]; files: string[] }>();

  // Try suffix-based grouping: *Handler, *Service, *Schema, *Bean, etc.
  const suffixes = ["Handler", "Service", "Schema", "Bean", "Manager", "Dao", "Entity", "Info"];
  const ungrouped: { classes: string[]; files: string[] } = { classes: [], files: [] };

  for (let i = 0; i < classes.length; i++) {
    const cls = classes[i];
    const file = files[i] ?? "";
    let matched = false;
    for (const suffix of suffixes) {
      if (cls.endsWith(suffix) || cls.includes(suffix)) {
        const groupName = suffix.toLowerCase() + "s";
        const group = groups.get(groupName) ?? { classes: [], files: [] };
        group.classes.push(cls);
        if (file) group.files.push(file);
        groups.set(groupName, group);
        matched = true;
        break;
      }
    }
    if (!matched) {
      ungrouped.classes.push(cls);
      if (file) ungrouped.files.push(file);
    }
  }

  if (ungrouped.classes.length > 0) {
    groups.set("other", ungrouped);
  }

  return groups;
}

/** Collect all leaf modules (depth-first order) */
export function getLeafModules(root: ModuleNode): ModuleNode[] {
  const leaves: ModuleNode[] = [];
  function walk(node: ModuleNode) {
    if (node.children.length === 0 && node.level !== "root") {
      leaves.push(node);
    } else {
      for (const child of node.children) {
        walk(child);
      }
    }
  }
  walk(root);
  return leaves;
}

/** Collect parent modules in bottom-up order (deepest parents first) */
export function getParentModulesBottomUp(root: ModuleNode): ModuleNode[] {
  const parents: ModuleNode[] = [];
  function walk(node: ModuleNode, depth: number) {
    for (const child of node.children) {
      walk(child, depth + 1);
    }
    if (node.children.length > 0 && node.level !== "root") {
      parents.push(node);
    }
  }
  walk(root, 0);
  return parents;
}
```

**Step 2: Export from SDK index**

Add to `src/lib/repopilot/index.ts`:

```typescript
// Module tree
export { buildModuleTree, getLeafModules, getParentModulesBottomUp } from "./module-tree";
export type { ModuleNode } from "./module-tree";
```

**Step 3: Verify build**

Run: `npx next build`
Expected: Build succeeds (no test yet — this is a pure library module)

**Step 4: Commit**

```bash
git add src/lib/repopilot/module-tree.ts src/lib/repopilot/index.ts
git commit -m "feat(codewiki): add module tree builder from knowledge graph"
```

---

### Task 2: Wiki Generator Orchestrator

**Files:**
- Create: `src/server/wiki-generator.ts`

**Context:**
- Read `src/lib/repopilot/scanner/scanner.ts` for `RepoScanner` API: `new RepoScanner(sourcePath).scan()` returns `RepoIndex`
- Read `src/lib/repopilot/graph/index.ts` for `RepoIndex.save(path)` and `RepoIndex.load(path)`
- Read `src/lib/repopilot/module-tree.ts` (Task 1) for `buildModuleTree()`, `getLeafModules()`, `getParentModulesBottomUp()`
- Read `src/server/agent.ts` for how `query()` from `@anthropic-ai/claude-agent-sdk` is used, env setup, `resolveClaudeCodePath()`
- The wiki generator runs Phase 1 (scan + tree), then Phase 2-3 (subagent wiki generation)

**Step 1: Create the wiki generator**

```typescript
// src/server/wiki-generator.ts
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { RepoScanner } from "@/lib/repopilot/scanner/scanner";
import { RepoIndex } from "@/lib/repopilot/graph/index";
import {
  buildModuleTree,
  getLeafModules,
  getParentModulesBottomUp,
  type ModuleNode,
} from "@/lib/repopilot/module-tree";
import type { KnowledgeGraph } from "@/lib/repopilot/graph/knowledge-graph";
import { NodeType, EdgeType } from "@/lib/repopilot/types";
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
import { resolveClaudeCodePath } from "./agent-utils";

export interface WikiGenerationResult {
  pagesGenerated: number;
  moduleTree: ModuleNode;
  errors: string[];
}

/**
 * Generate wiki documentation for a repository.
 * Phase 1: Scan code → build graph → build module tree
 * Phase 2-3: Multi-agent documentation generation (leaves → parents → root)
 */
export async function generateWiki(
  sourcePath: string,
  onProgress?: (message: string) => void,
): Promise<WikiGenerationResult> {
  const errors: string[] = [];
  const log = (msg: string) => {
    console.log(`[wiki] ${msg}`);
    onProgress?.(msg);
  };

  // ── Phase 1: Scan & Decompose ──
  log("Phase 1: Scanning repository...");

  const scanner = new RepoScanner(sourcePath);
  const index = scanner.scan();

  const rpidxPath = join(sourcePath, "index.rpidx");
  index.save(rpidxPath);
  log(`Knowledge graph saved: ${index.graph.nodeCount} nodes, ${index.graph.edgeCount} edges`);

  const moduleTree = buildModuleTree(index.graph);
  const wikiDir = join(sourcePath, ".codewiki");
  mkdirSync(wikiDir, { recursive: true });
  writeFileSync(join(wikiDir, "tree.json"), JSON.stringify(moduleTree, null, 2));
  log(`Module tree built: ${countModules(moduleTree)} modules`);

  // ── Phase 2: Generate leaf docs ──
  log("Phase 2: Generating leaf module documentation...");

  const leaves = getLeafModules(moduleTree);
  let pagesGenerated = 0;

  for (const leaf of leaves) {
    try {
      log(`  Documenting: ${leaf.path}`);
      await generateModuleDoc(leaf, moduleTree, index.graph, sourcePath, wikiDir);
      leaf.status = "done";
      pagesGenerated++;
    } catch (err) {
      const msg = `Failed to document ${leaf.path}: ${err instanceof Error ? err.message : err}`;
      errors.push(msg);
      console.error(`[wiki] ${msg}`);
    }
  }

  // ── Phase 3: Synthesize parent docs bottom-up ──
  log("Phase 3: Synthesizing parent documentation...");

  const parents = getParentModulesBottomUp(moduleTree);
  for (const parent of parents) {
    try {
      log(`  Synthesizing: ${parent.path}`);
      await synthesizeParentDoc(parent, moduleTree, index.graph, sourcePath, wikiDir);
      parent.status = "done";
      pagesGenerated++;
    } catch (err) {
      const msg = `Failed to synthesize ${parent.path}: ${err instanceof Error ? err.message : err}`;
      errors.push(msg);
      console.error(`[wiki] ${msg}`);
    }
  }

  // Root overview
  try {
    log("  Synthesizing: overview");
    await synthesizeRootOverview(moduleTree, index, sourcePath, wikiDir);
    pagesGenerated++;
  } catch (err) {
    const msg = `Failed to generate overview: ${err instanceof Error ? err.message : err}`;
    errors.push(msg);
    console.error(`[wiki] ${msg}`);
  }

  // Update tree.json with final statuses
  writeFileSync(join(wikiDir, "tree.json"), JSON.stringify(moduleTree, null, 2));
  log(`Wiki generation complete: ${pagesGenerated} pages, ${errors.length} errors`);

  return { pagesGenerated, moduleTree, errors };
}

/**
 * Generate documentation for a leaf module using a module-documenter subagent.
 */
async function generateModuleDoc(
  module: ModuleNode,
  tree: ModuleNode,
  graph: KnowledgeGraph,
  sourcePath: string,
  wikiDir: string,
): Promise<void> {
  const graphContext = buildGraphContext(module, graph);
  const treeContext = buildTreeContext(module, tree);

  const prompt = `You are a code documentation expert. Generate a comprehensive markdown wiki page for this code module.

## Module: ${module.name}
**Path in module tree:** ${module.path}
**Level:** ${module.level}

## Module Tree Context
${treeContext}

## Graph Analysis
${graphContext}

## Instructions
1. Read ALL source files listed below using the Read tool
2. Analyze the code thoroughly: classes, methods, patterns, dependencies
3. Write a comprehensive markdown wiki page
4. Save it to: ${join(wikiDir, module.docPath!)} using the Write tool

## Source Files to Read
${module.files.map((f) => `- ${f}`).join("\n")}

## Wiki Page Structure
Write the page with these sections:
- **Overview**: Module purpose and responsibility (2-3 sentences)
- **Classes**: For each class: purpose, key methods, important fields
- **Patterns**: Design patterns, inheritance hierarchies, message flows
- **Dependencies**: What this module depends on and what depends on it
- **Key Code Examples**: Important code snippets with explanations

Use mermaid diagrams where helpful (class diagrams, sequence diagrams).`;

  await runSubagent(prompt, sourcePath, "module-documenter");
}

/**
 * Synthesize a parent module doc from its children's documentation.
 */
async function synthesizeParentDoc(
  module: ModuleNode,
  tree: ModuleNode,
  graph: KnowledgeGraph,
  sourcePath: string,
  wikiDir: string,
): Promise<void> {
  const childDocPaths = module.children
    .filter((c) => c.docPath)
    .map((c) => join(wikiDir, c.docPath!));

  const treeContext = buildTreeContext(module, tree);

  const prompt = `You are a documentation architect. Synthesize a parent-level overview page from the child module documentation.

## Module: ${module.name}
**Path in module tree:** ${module.path}

## Module Tree Context
${treeContext}

## Instructions
1. Read ALL child documentation files listed below using the Read tool
2. Synthesize a higher-level overview that explains how these sub-modules work together
3. Save it to: ${join(wikiDir, module.docPath!)} using the Write tool

## Child Documentation to Read
${childDocPaths.map((p) => `- ${p}`).join("\n")}

## Wiki Page Structure
- **Overview**: Package purpose and responsibility
- **Sub-modules**: Brief summary of each child module and its role
- **Architecture**: How the sub-modules interact and depend on each other
- **Data Flow**: How data flows through this package (mermaid diagram)
- **Key Abstractions**: Important interfaces, base classes, patterns`;

  await runSubagent(prompt, sourcePath, "module-synthesizer");
}

/**
 * Synthesize the root overview from all top-level package docs.
 */
async function synthesizeRootOverview(
  tree: ModuleNode,
  index: RepoIndex,
  sourcePath: string,
  wikiDir: string,
): Promise<void> {
  const topDocPaths = tree.children
    .filter((c) => c.docPath)
    .map((c) => join(wikiDir, c.docPath!));

  const meta = index.metadata;
  const treeJson = JSON.stringify(tree, null, 2);

  const prompt = `You are a documentation architect. Create the root overview page for this entire repository.

## Repository Stats
- Total files: ${meta.file_count ?? "unknown"}
- Total nodes in graph: ${meta.node_count ?? "unknown"}
- Total edges in graph: ${meta.edge_count ?? "unknown"}
- Scan time: ${meta.scan_time_seconds ?? "unknown"}s

## Full Module Tree
\`\`\`json
${treeJson}
\`\`\`

## Instructions
1. Read ALL top-level package documentation files listed below using the Read tool
2. Create a comprehensive repository overview
3. Save it to: ${join(wikiDir, "overview.md")} using the Write tool

## Package Documentation to Read
${topDocPaths.map((p) => `- ${p}`).join("\n")}

## Wiki Page Structure
- **Repository Overview**: Purpose, domain, technology stack
- **Architecture**: High-level system architecture (mermaid diagram)
- **Packages**: Each top-level package with its purpose and key components
- **Key Patterns**: Framework patterns, message flows, data access patterns
- **Dependencies**: How packages depend on each other (mermaid diagram)
- **Entry Points**: Main classes, EJB endpoints, key interfaces`;

  await runSubagent(prompt, sourcePath, "module-synthesizer");
}

/**
 * Run a subagent with the given prompt using Claude Agent SDK.
 */
async function runSubagent(
  prompt: string,
  cwd: string,
  agentType: string,
): Promise<void> {
  const claudeCodePath = resolveClaudeCodePath();

  const options: Options = {
    cwd,
    allowedTools: ["Read", "Glob", "Grep", "Write"],
    permissionMode: "bypassPermissions",
    allowDangerouslySkipPermissions: true,
    model: process.env.LLM_MODEL || "gpt-5-mini",
    maxTurns: 20,
    ...(claudeCodePath ? { pathToClaudeCodeExecutable: claudeCodePath } : {}),
    env: {
      PATH: process.env.PATH || "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      HOME: process.env.HOME || "/home/repopilot",
      ...Object.fromEntries(
        Object.entries(process.env).filter((e): e is [string, string] => e[1] != null),
      ),
      ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || process.env.LITELLM_PROXY_URL || "",
      ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy",
    },
  };

  for await (const message of query({ prompt, options })) {
    const msgType = (message as Record<string, unknown>).type as string;
    if (msgType === "result") {
      const result = message as { is_error?: boolean; result?: string };
      if (result.is_error) {
        throw new Error(`${agentType} failed: ${result.result}`);
      }
    }
  }
}

/**
 * Build graph context string for a module.
 */
function buildGraphContext(module: ModuleNode, graph: KnowledgeGraph): string {
  const lines: string[] = [];
  lines.push(`Classes: ${module.classes.join(", ") || "none"}`);
  lines.push(`Files: ${module.files.length}`);

  // Get relationships for module classes
  for (const cls of module.classes) {
    const nodes = graph.searchNodes(cls, [NodeType.CLASS, NodeType.INTERFACE, NodeType.ENUM]);
    if (nodes.length === 0) continue;
    const nodeId = nodes[0];
    const attrs = graph.getNodeAttrs(nodeId);

    const extends_ = graph.getNeighbors(nodeId, EdgeType.EXTENDS, "out");
    const implements_ = graph.getNeighbors(nodeId, EdgeType.IMPLEMENTS, "out");
    const methods = graph.getNeighbors(nodeId, EdgeType.CONTAINS, "out");

    const parts: string[] = [`${attrs.type} ${cls}`];
    if (extends_.length > 0) {
      parts.push(`extends: ${extends_.map((id) => { try { return graph.getNodeAttrs(id).name; } catch { return id; } }).join(", ")}`);
    }
    if (implements_.length > 0) {
      parts.push(`implements: ${implements_.map((id) => { try { return graph.getNodeAttrs(id).name; } catch { return id; } }).join(", ")}`);
    }
    parts.push(`members: ${methods.length}`);
    lines.push(parts.join(" | "));
  }

  return lines.join("\n");
}

/**
 * Build module tree context showing the module's position.
 */
function buildTreeContext(module: ModuleNode, tree: ModuleNode): string {
  const lines: string[] = [];
  lines.push(`Module: ${module.path} (${module.level})`);
  if (module.parent) lines.push(`Parent: ${module.parent}`);
  if (module.children.length > 0) {
    lines.push(`Children: ${module.children.map((c) => c.name).join(", ")}`);
  }

  // Siblings
  const parent = findModule(tree, module.parent || "root");
  if (parent) {
    const siblings = parent.children.filter((c) => c.id !== module.id);
    if (siblings.length > 0) {
      lines.push(`Siblings: ${siblings.map((s) => s.name).join(", ")}`);
    }
  }

  return lines.join("\n");
}

function findModule(tree: ModuleNode, id: string): ModuleNode | null {
  if (tree.id === id) return tree;
  for (const child of tree.children) {
    const found = findModule(child, id);
    if (found) return found;
  }
  return null;
}

function countModules(tree: ModuleNode): number {
  let count = tree.level !== "root" ? 1 : 0;
  for (const child of tree.children) {
    count += countModules(child);
  }
  return count;
}
```

**Step 2: Extract resolveClaudeCodePath to shared utility**

Create `src/server/agent-utils.ts`:

```typescript
// src/server/agent-utils.ts
import { existsSync } from "node:fs";
import { join, dirname } from "node:path";

export function resolveClaudeCodePath(): string | undefined {
  try {
    let dir = __dirname;
    for (let i = 0; i < 10; i++) {
      const candidate = join(dir, "node_modules", "@anthropic-ai", "claude-agent-sdk", "cli.js");
      if (existsSync(candidate)) return candidate;
      const parent = dirname(dir);
      if (parent === dir) break;
      dir = parent;
    }
  } catch { /* not resolvable */ }

  const globalPaths = [
    "/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js",
    "/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js",
  ];
  for (const p of globalPaths) {
    if (existsSync(p)) return p;
  }

  return undefined;
}
```

Update `src/server/agent.ts` to import from the shared utility:

Replace the `resolveClaudeCodePath` function definition with:
```typescript
import { resolveClaudeCodePath } from "./agent-utils";
```

And remove the old `resolveClaudeCodePath` function from `agent.ts`.

**Step 3: Verify build**

Run: `npx next build`
Expected: Build succeeds

**Step 4: Commit**

```bash
git add src/server/wiki-generator.ts src/server/agent-utils.ts src/server/agent.ts
git commit -m "feat(codewiki): add wiki generator orchestrator with subagent support"
```

---

### Task 3: Integrate Wiki Generation into Repository Upload

**Files:**
- Modify: `src/app/api/repositories/route.ts`

**Context:**
- Read `src/app/api/repositories/route.ts` — current POST handler
- Read `src/server/wiki-generator.ts` (Task 2) for `generateWiki(sourcePath)`
- Wiki generation is synchronous — the POST response waits for all docs to be generated

**Step 1: Add wiki generation after file extraction**

In `src/app/api/repositories/route.ts`, add the import at the top:

```typescript
import { generateWiki } from "@/server/wiki-generator";
```

Then, after the DB insert (`const { rows } = await db.query(...)`) and before `return NextResponse.json(rows[0], { status: 201 })`, add:

```typescript
  // Generate wiki documentation
  try {
    const wikiResult = await generateWiki(sourcePath);
    console.log(
      `[repo.wiki] id=${rows[0].id} pages=${wikiResult.pagesGenerated} errors=${wikiResult.errors.length}`
    );
  } catch (err) {
    console.error(
      `[repo.wiki.failed] ${err instanceof Error ? err.message : err}`
    );
    // Don't fail the upload — wiki generation is best-effort
  }
```

**Step 2: Verify build**

Run: `npx next build`
Expected: Build succeeds

**Step 3: Commit**

```bash
git add src/app/api/repositories/route.ts
git commit -m "feat(codewiki): trigger wiki generation on repository upload"
```

---

### Task 4: Wiki API Routes

**Files:**
- Create: `src/app/api/repositories/[id]/wiki/route.ts`
- Create: `src/app/api/repositories/[id]/wiki/[...path]/route.ts`

**Context:**
- Read `src/app/api/repositories/[id]/route.ts` for the existing pattern of reading repository by id, getting `source_path` from DB
- Wiki pages are stored as `.md` files in `{source_path}/.codewiki/`
- `tree.json` is at `{source_path}/.codewiki/tree.json`
- Next.js 15 dynamic routes: `params` is `Promise<{ id: string }>` (must be awaited)

**Step 1: Create wiki tree endpoint**

```typescript
// src/app/api/repositories/[id]/wiki/route.ts
import { NextResponse } from "next/server";
import { getDb } from "@/lib/db";
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const db = await getDb();
  const { rows } = await db.query(
    "SELECT source_path FROM repositories WHERE id = $1",
    [id],
  );
  if (rows.length === 0) {
    return NextResponse.json({ error: "Repository not found" }, { status: 404 });
  }

  const treePath = join(rows[0].source_path, ".codewiki", "tree.json");
  if (!existsSync(treePath)) {
    return NextResponse.json({ error: "Wiki not generated yet" }, { status: 404 });
  }

  const tree = JSON.parse(readFileSync(treePath, "utf-8"));
  return NextResponse.json(tree);
}
```

**Step 2: Create wiki page content endpoint**

```typescript
// src/app/api/repositories/[id]/wiki/[...path]/route.ts
import { NextResponse } from "next/server";
import { getDb } from "@/lib/db";
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ id: string; path: string[] }> },
) {
  const { id, path: pathSegments } = await params;
  const db = await getDb();
  const { rows } = await db.query(
    "SELECT source_path FROM repositories WHERE id = $1",
    [id],
  );
  if (rows.length === 0) {
    return NextResponse.json({ error: "Repository not found" }, { status: 404 });
  }

  const pagePath = pathSegments.join("/");
  const filePath = join(rows[0].source_path, ".codewiki", `${pagePath}.md`);

  if (!existsSync(filePath)) {
    return NextResponse.json({ error: "Wiki page not found" }, { status: 404 });
  }

  const content = readFileSync(filePath, "utf-8");
  return NextResponse.json({ path: pagePath, content });
}
```

**Step 3: Verify build**

Run: `npx next build`
Expected: Build succeeds

**Step 4: Commit**

```bash
git add src/app/api/repositories/[id]/wiki/
git commit -m "feat(codewiki): add wiki API routes for tree and page content"
```

---

### Task 5: Tab Bar Component

**Files:**
- Create: `src/components/TabBar.tsx`

**Context:**
- Read `src/components/ChatApp.tsx` for current layout structure
- Three tabs: Chat, Code, Wiki
- TailwindCSS classes following the existing `rp-*` design tokens (see ChatView.tsx for examples: `bg-rp-bg`, `text-rp-text`, `text-rp-muted`, `bg-rp-surface`, `border-rp-border`)

**Step 1: Create the TabBar component**

```tsx
// src/components/TabBar.tsx
"use client";

export type TabId = "chat" | "code" | "wiki";

interface TabBarProps {
  activeTab: TabId;
  onTabChange: (tab: TabId) => void;
  wikiAvailable: boolean;
}

const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
  {
    id: "chat",
    label: "Chat",
    icon: (
      <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
        <path d="M2 3h12a1 1 0 011 1v7a1 1 0 01-1 1H5l-3 3V4a1 1 0 011-1z" />
      </svg>
    ),
  },
  {
    id: "code",
    label: "Code",
    icon: (
      <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
        <polyline points="5 4 1 8 5 12" />
        <polyline points="11 4 15 8 11 12" />
        <line x1="9" y1="2" x2="7" y2="14" />
      </svg>
    ),
  },
  {
    id: "wiki",
    label: "Wiki",
    icon: (
      <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
        <path d="M2 2h8l4 4v8a1 1 0 01-1 1H2a1 1 0 01-1-1V3a1 1 0 011-1z" />
        <path d="M10 2v4h4" />
        <line x1="4" y1="8" x2="10" y2="8" />
        <line x1="4" y1="10.5" x2="10" y2="10.5" />
        <line x1="4" y1="13" x2="8" y2="13" />
      </svg>
    ),
  },
];

export function TabBar({ activeTab, onTabChange, wikiAvailable }: TabBarProps) {
  return (
    <div className="flex items-center gap-1 px-3 border-b border-rp-border bg-rp-bg">
      {tabs.map((tab) => {
        const isActive = activeTab === tab.id;
        const isDisabled = tab.id === "wiki" && !wikiAvailable;

        return (
          <button
            key={tab.id}
            onClick={() => !isDisabled && onTabChange(tab.id)}
            disabled={isDisabled}
            className={`flex items-center gap-1.5 px-3 py-2 text-r-sm font-medium transition-colors border-b-2 -mb-px ${
              isActive
                ? "border-rp-accent text-rp-accent"
                : isDisabled
                  ? "border-transparent text-rp-muted/50 cursor-not-allowed"
                  : "border-transparent text-rp-muted hover:text-rp-text hover:border-rp-border"
            }`}
            title={isDisabled ? "Wiki not generated yet" : undefined}
          >
            {tab.icon}
            {tab.label}
          </button>
        );
      })}
    </div>
  );
}
```

**Step 2: Verify build**

Run: `npx next build`
Expected: Build succeeds

**Step 3: Commit**

```bash
git add src/components/TabBar.tsx
git commit -m "feat(codewiki): add TabBar component for Chat/Code/Wiki tabs"
```

---

### Task 6: Wiki View Components

**Files:**
- Create: `src/components/WikiTree.tsx`
- Create: `src/components/WikiPage.tsx`
- Create: `src/components/WikiView.tsx`
- Create: `src/hooks/useWiki.ts`

**Context:**
- Read `src/components/ChatView.tsx` for markdown rendering approach (ReactMarkdown, remarkGfm, MermaidDiagram, code block styling)
- Read `src/components/FileTree.tsx` for tree rendering pattern
- Wiki tree comes from `/api/repositories/[id]/wiki` (tree.json)
- Wiki page content from `/api/repositories/[id]/wiki/[...path]`
- The `ModuleNode` type has: id, name, path, level, children, docPath, status

**Step 1: Create the wiki hook**

```typescript
// src/hooks/useWiki.ts
"use client";

import { useState, useEffect, useCallback } from "react";
import type { ModuleNode } from "@/lib/repopilot/module-tree";

export function useWiki(repositoryId: string | null) {
  const [tree, setTree] = useState<ModuleNode | null>(null);
  const [selectedPage, setSelectedPage] = useState<string | null>(null);
  const [pageContent, setPageContent] = useState<string>("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Fetch wiki tree when repository changes
  useEffect(() => {
    if (!repositoryId) {
      setTree(null);
      setSelectedPage(null);
      setPageContent("");
      return;
    }

    fetch(`/api/repositories/${repositoryId}/wiki`)
      .then((res) => {
        if (!res.ok) {
          setTree(null);
          return;
        }
        return res.json();
      })
      .then((data) => {
        if (data) setTree(data);
      })
      .catch(() => setTree(null));
  }, [repositoryId]);

  // Fetch page content when selected page changes
  const selectPage = useCallback(
    async (pagePath: string) => {
      if (!repositoryId) return;
      setSelectedPage(pagePath);
      setLoading(true);
      setError(null);

      try {
        const res = await fetch(
          `/api/repositories/${repositoryId}/wiki/${pagePath}`,
        );
        if (!res.ok) {
          setError("Failed to load wiki page");
          setPageContent("");
          return;
        }
        const data = await res.json();
        setPageContent(data.content);
      } catch {
        setError("Failed to load wiki page");
        setPageContent("");
      } finally {
        setLoading(false);
      }
    },
    [repositoryId],
  );

  const isAvailable = tree !== null;

  return { tree, selectedPage, pageContent, loading, error, selectPage, isAvailable };
}
```

**Step 2: Create the WikiTree component**

```tsx
// src/components/WikiTree.tsx
"use client";

import { useState } from "react";
import type { ModuleNode } from "@/lib/repopilot/module-tree";

interface WikiTreeProps {
  tree: ModuleNode;
  selectedPage: string | null;
  onSelectPage: (pagePath: string) => void;
}

export function WikiTree({ tree, selectedPage, onSelectPage }: WikiTreeProps) {
  return (
    <div className="w-64 shrink-0 border-r border-rp-border overflow-y-auto bg-rp-bg p-2">
      {/* Overview page */}
      <TreeItem
        label="Overview"
        pagePath="overview"
        icon="📖"
        isSelected={selectedPage === "overview"}
        onSelect={onSelectPage}
      />
      {/* Module tree */}
      {tree.children.map((child) => (
        <TreeNode
          key={child.id}
          node={child}
          selectedPage={selectedPage}
          onSelectPage={onSelectPage}
          depth={0}
        />
      ))}
    </div>
  );
}

function TreeNode({
  node,
  selectedPage,
  onSelectPage,
  depth,
}: {
  node: ModuleNode;
  selectedPage: string | null;
  onSelectPage: (pagePath: string) => void;
  depth: number;
}) {
  const [expanded, setExpanded] = useState(depth < 1);
  const hasChildren = node.children.length > 0;
  const pagePath = node.docPath?.replace(/\.md$/, "") ?? node.path;

  return (
    <div>
      <div className="flex items-center">
        {hasChildren && (
          <button
            onClick={() => setExpanded(!expanded)}
            className="w-5 h-5 flex items-center justify-center text-rp-muted hover:text-rp-text shrink-0"
          >
            <svg
              width="12"
              height="12"
              viewBox="0 0 12 12"
              fill="currentColor"
              className={`transition-transform ${expanded ? "rotate-90" : ""}`}
            >
              <path d="M4 2l4 4-4 4z" />
            </svg>
          </button>
        )}
        <TreeItem
          label={node.name}
          pagePath={pagePath}
          icon={hasChildren ? "📁" : "📄"}
          isSelected={selectedPage === pagePath}
          onSelect={onSelectPage}
          indent={!hasChildren}
        />
      </div>
      {expanded && hasChildren && (
        <div className="ml-3">
          {node.children.map((child) => (
            <TreeNode
              key={child.id}
              node={child}
              selectedPage={selectedPage}
              onSelectPage={onSelectPage}
              depth={depth + 1}
            />
          ))}
        </div>
      )}
    </div>
  );
}

function TreeItem({
  label,
  pagePath,
  icon,
  isSelected,
  onSelect,
  indent,
}: {
  label: string;
  pagePath: string;
  icon: string;
  isSelected: boolean;
  onSelect: (path: string) => void;
  indent?: boolean;
}) {
  return (
    <button
      onClick={() => onSelect(pagePath)}
      className={`flex items-center gap-1.5 w-full text-left px-2 py-1 rounded text-r-sm transition-colors ${
        indent ? "ml-5" : ""
      } ${
        isSelected
          ? "bg-rp-accent/10 text-rp-accent"
          : "text-rp-text hover:bg-rp-surface"
      }`}
    >
      <span className="text-xs">{icon}</span>
      <span className="truncate">{label}</span>
    </button>
  );
}
```

**Step 3: Create the WikiPage component**

```tsx
// src/components/WikiPage.tsx
"use client";

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { MermaidDiagram } from "@/components/markdown/MermaidDiagram";

interface WikiPageProps {
  content: string;
  loading: boolean;
  error: string | null;
}

export function WikiPage({ content, loading, error }: WikiPageProps) {
  if (loading) {
    return (
      <div className="flex items-center justify-center h-full text-rp-muted">
        Loading page...
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex items-center justify-center h-full text-red-400">
        {error}
      </div>
    );
  }

  if (!content) {
    return (
      <div className="flex items-center justify-center h-full text-rp-muted">
        Select a wiki page from the tree
      </div>
    );
  }

  return (
    <div className="flex-1 overflow-y-auto p-6 max-w-4xl mx-auto">
      <article className="prose prose-invert prose-sm max-w-none">
        <ReactMarkdown
          remarkPlugins={[remarkGfm]}
          components={{
            code({ className, children, ...props }) {
              const match = /language-(\w+)/.exec(className || "");
              const lang = match?.[1];
              const codeText = String(children).replace(/\n$/, "");

              if (lang === "mermaid") {
                return <MermaidDiagram chart={codeText} />;
              }

              if (lang) {
                return (
                  <div className="relative group">
                    <div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
                      <button
                        onClick={() => navigator.clipboard.writeText(codeText)}
                        className="px-2 py-1 text-xs bg-rp-surface text-rp-muted rounded hover:text-rp-text"
                      >
                        Copy
                      </button>
                    </div>
                    <pre className="overflow-x-auto">
                      <code className={className} {...props}>
                        {children}
                      </code>
                    </pre>
                  </div>
                );
              }

              return (
                <code className="bg-rp-surface px-1.5 py-0.5 rounded text-rp-accent text-r-xs" {...props}>
                  {children}
                </code>
              );
            },
            table({ children }) {
              return (
                <div className="overflow-x-auto">
                  <table className="w-full">{children}</table>
                </div>
              );
            },
          }}
        />
      </article>
    </div>
  );
}
```

**Step 4: Create the WikiView composite component**

```tsx
// src/components/WikiView.tsx
"use client";

import { WikiTree } from "./WikiTree";
import { WikiPage } from "./WikiPage";
import type { ModuleNode } from "@/lib/repopilot/module-tree";

interface WikiViewProps {
  tree: ModuleNode;
  selectedPage: string | null;
  pageContent: string;
  loading: boolean;
  error: string | null;
  onSelectPage: (path: string) => void;
}

export function WikiView({
  tree,
  selectedPage,
  pageContent,
  loading,
  error,
  onSelectPage,
}: WikiViewProps) {
  return (
    <div className="flex h-full">
      <WikiTree
        tree={tree}
        selectedPage={selectedPage}
        onSelectPage={onSelectPage}
      />
      <WikiPage content={pageContent} loading={loading} error={error} />
    </div>
  );
}
```

**Step 5: Verify build**

Run: `npx next build`
Expected: Build succeeds

**Step 6: Commit**

```bash
git add src/hooks/useWiki.ts src/components/WikiTree.tsx src/components/WikiPage.tsx src/components/WikiView.tsx
git commit -m "feat(codewiki): add WikiView components and useWiki hook"
```

---

### Task 7: Integrate Tabs into ChatApp

**Files:**
- Modify: `src/components/ChatApp.tsx`

**Context:**
- Read `src/components/ChatApp.tsx` — current layout with sidebar + ChatView
- Read `src/components/TabBar.tsx` (Task 5) for `TabBar`, `TabId`
- Read `src/components/WikiView.tsx` (Task 6) for `WikiView`
- Read `src/hooks/useWiki.ts` (Task 6) for `useWiki`
- Read `src/components/CodeView.tsx` and `src/components/FileTree.tsx` for existing code browser components

**Step 1: Update ChatApp with tabs**

Replace the full content of `src/components/ChatApp.tsx` with:

```tsx
"use client";

import { useState, useEffect, useRef } from "react";
import { useAgent } from "@/hooks/useAgent";
import { useSessionManager } from "@/hooks/useSessionManager";
import { useRepositories } from "@/hooks/useRepositories";
import { useWiki } from "@/hooks/useWiki";
import { ChatView } from "@/components/ChatView";
import { ChatSidebar } from "@/components/sidebar/ChatSidebar";
import { RepositoryModal } from "@/components/RepositoryModal";
import { TabBar, type TabId } from "@/components/TabBar";
import { WikiView } from "@/components/WikiView";

export default function ChatApp() {
  const { repositories, addRepository, deleteRepository } = useRepositories();
  const sessionManager = useSessionManager();
  const [showRepoModal, setShowRepoModal] = useState(false);
  const [activeTab, setActiveTab] = useState<TabId>("chat");
  const wiki = useWiki(sessionManager.repositoryId);

  const {
    messages,
    isStreaming,
    error,
    pendingInterrupt,
    analysisState,
    sendMessage,
    resolveInterrupt,
    resetSession,
  } = useAgent({
    onMessagesChange: sessionManager.persistMessages,
    repositoryId: sessionManager.repositoryId,
  });

  const initializedRef = useRef(false);

  useEffect(() => {
    if (!sessionManager.repositoryId && repositories.length > 0) {
      sessionManager.setRepositoryId(repositories[0].id);
    }
  }, [repositories, sessionManager.repositoryId, sessionManager]);

  useEffect(() => {
    const session = sessionManager.activeSession;
    if (!session) return;
    resetSession({
      messages: session.messages,
      history: session.agui_history,
      threadId: session.thread_id,
    });
    initializedRef.current = true;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sessionManager.activeSessionId]);

  return (
    <div className="flex h-full bg-rp-bg">
      {sessionManager.sidebarOpen && (
        <ChatSidebar
          groupedSessions={sessionManager.groupedSessions}
          activeSessionId={sessionManager.activeSessionId}
          onSelectSession={sessionManager.switchSession}
          onDeleteSession={sessionManager.deleteSessionById}
          onNewSession={sessionManager.createNewSession}
          onClose={sessionManager.toggleSidebar}
          repositories={repositories}
          selectedRepositoryId={sessionManager.repositoryId}
          onSelectRepository={sessionManager.setRepositoryId}
          onManageRepositories={() => setShowRepoModal(true)}
        />
      )}

      <div className="flex flex-col flex-1 min-w-0">
        <div className="h-10 flex items-center px-3 shrink-0">
          <button
            onClick={sessionManager.toggleSidebar}
            className="text-rp-muted hover:text-rp-text transition-colors p-1.5 rounded-lg hover:bg-rp-surface"
            title={sessionManager.sidebarOpen ? "Close sidebar" : "Open sidebar"}
          >
            <SidebarIcon />
          </button>
        </div>

        {sessionManager.repositoryId ? (
          <>
            <TabBar
              activeTab={activeTab}
              onTabChange={setActiveTab}
              wikiAvailable={wiki.isAvailable}
            />
            <div className="flex-1 min-h-0">
              {activeTab === "chat" && (
                <ChatView
                  messages={messages}
                  isStreaming={isStreaming}
                  error={error}
                  pendingInterrupt={pendingInterrupt}
                  analysisState={analysisState}
                  onSend={sendMessage}
                  onResolveInterrupt={resolveInterrupt}
                />
              )}
              {activeTab === "code" && (
                <div className="flex items-center justify-center h-full text-rp-muted">
                  Code browser coming soon
                </div>
              )}
              {activeTab === "wiki" && wiki.tree && (
                <WikiView
                  tree={wiki.tree}
                  selectedPage={wiki.selectedPage}
                  pageContent={wiki.pageContent}
                  loading={wiki.loading}
                  error={wiki.error}
                  onSelectPage={wiki.selectPage}
                />
              )}
            </div>
          </>
        ) : (
          <div className="flex-1 min-h-0 flex items-center justify-center">
            <div className="text-center">
              <p className="text-rp-muted text-r-sm mb-3">No repository selected</p>
              <button
                onClick={() => setShowRepoModal(true)}
                className="px-4 py-2 bg-rp-accent text-white text-r-sm rounded-lg hover:bg-rp-accent/90 transition-colors"
              >
                Add Repository
              </button>
            </div>
          </div>
        )}
      </div>

      {showRepoModal && (
        <RepositoryModal
          repositories={repositories}
          onAdd={addRepository}
          onDelete={deleteRepository}
          onClose={() => setShowRepoModal(false)}
        />
      )}
    </div>
  );
}

function SidebarIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
      <rect x="2" y="3" width="14" height="12" rx="2" />
      <path d="M7 3V15" />
    </svg>
  );
}
```

**Step 2: Verify build**

Run: `npx next build`
Expected: Build succeeds

**Step 3: Commit**

```bash
git add src/components/ChatApp.tsx
git commit -m "feat(codewiki): integrate three-tab layout (Chat/Code/Wiki) into ChatApp"
```

---

### Task 8: Docker Build & End-to-End Test

**Files:**
- Modify: `Dockerfile` (if needed for tree-sitter native modules)
- Modify: `next.config.ts` (if needed for new server modules)

**Context:**
- The wiki generator uses `tree-sitter` and `tree-sitter-java` which are already in `serverExternalPackages` in `next.config.ts`
- The wiki generator runs server-side in the API route
- Docker build must succeed with all new modules

**Step 1: Verify next.config.ts has all needed packages**

Check `next.config.ts` — it should already have `tree-sitter`, `tree-sitter-java`, `@msgpack/msgpack`, `pg`, `@anthropic-ai/claude-agent-sdk`. No changes needed if all present.

**Step 2: Build Docker image**

Run: `docker-compose build repopilot`
Expected: Build succeeds without errors

**Step 3: Start services**

Run: `docker-compose up -d`
Expected: Both postgres and repopilot containers start

**Step 4: Test wiki generation**

Upload a test repository via API and verify wiki pages are generated:

```bash
# Check the wiki for the existing K-Opticom repo (if still in DB)
# Or upload Source.zip again and verify wiki generation
curl -s http://localhost:3000/api/repositories | jq '.[0].id'

# Get the repo ID, then check wiki
REPO_ID=$(curl -s http://localhost:3000/api/repositories | jq -r '.[0].id')
curl -s http://localhost:3000/api/repositories/$REPO_ID/wiki | jq '.children | length'

# Check a wiki page
curl -s http://localhost:3000/api/repositories/$REPO_ID/wiki/overview | jq '.content' | head -20
```

**Step 5: Test UI**

Open http://localhost:3000 in browser:
1. Select the repository from the dropdown
2. Verify three tabs appear: Chat, Code, Wiki
3. Click Wiki tab — should show tree navigation + page content
4. Click pages in the tree — content should load

**Step 6: Commit**

```bash
git add -A
git commit -m "feat(codewiki): complete wiki generation pipeline with Docker integration"
```
