# Bridge --review-only Implementation Plan

> **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:** Deliver `bridge --review-only` that runs a code-review turn via Host Agent, parses/scores the fixture response, and exits `passed` / `blocked` / `needs_host_agent` — with zero commit/push and zero external LLM HTTP calls.

**Architecture:** Port CLI preflight (repo + staged diff) and review parse/score logic by hand. Replace CLI `ReviewProvider` with `HostAgentClient.review` (`purpose: "code-review"`). Plan 2 supports **single-stage** review only (no chunked/summary turns). Full `bridge` without `--review-only` is deferred to Plan 3 and must error clearly.

**Tech Stack:** TypeScript 5.6, Node 20+, `node:test`, existing Plan 1 turn session (`HostAgentClient` / `SessionStore`).

**Spec:** `docs/superpowers/specs/2026-08-10-host-agent-design.md`  
**Roadmap:** `docs/superpowers/plans/2026-08-10-host-agent-roadmap.md`  
**Reference CLI (read-only):** `/Users/nietao/VSCode-plugins/smart-commit-cli` @ `0.1.21`  
**Key reference files:**
- `src/commands/bridge.ts` — review-only flow, payload shape, truncateDiff
- `src/review/parser.ts` — parse/validate/shouldBlock
- `src/review/prompt.ts` — `buildReviewMessages` (simplify: no skills/domain)
- `src/git.ts` — staged diff / auto-stage helpers
- `src/commands/types.ts` — Bridge payload types

**Plan 2 YAGNI (explicitly out of scope):**
- Full bridge commit/push/PR creation (Plan 3)
- Chunked review / summary turn
- Review skills / domain classifier / line-number annotation
- Pass-history writes and staged-change-summary generation (stub fields as disabled/skipped)
- Language validation of review text (accept parsed JSON as-is after structural validate)
- Correction repair turns on invalid JSON (invalid response → `RUNTIME_ERROR`; Plan 3+ may add repair)

---

## File structure (Plan 2)

| Path | Responsibility |
|------|----------------|
| `src/git.ts` | Minimal git helpers for review-only preflight |
| `src/review/types.ts` | Review input/result/detail types |
| `src/review/parser.ts` | Parse JSON review, score/decision, `shouldBlockReviewResult` |
| `src/review/prompt.ts` | Build single-stage review messages + responseSchema string |
| `src/review/hostAgentReview.ts` | Call `HostAgentClient.review`, parse, apply threshold |
| `src/commands/bridgeTypes.ts` | Bridge JSON payload types (host-agent) |
| `src/commands/bridge.ts` | `bridge --review-only` command orchestration |
| `src/cliApp.ts` | Parse/dispatch `bridge` async |
| `src/config/schema.ts` / `defaults.ts` / `load.ts` | Add `commitMessage.input` |
| `src/test/git.test.ts` | Git helper unit tests (temp repo) |
| `src/test/reviewParser.test.ts` | Parser / shouldBlock unit tests |
| `src/test/bridgeReviewOnly.test.ts` | End-to-end fixture: needs → passed/blocked |
| `docs/parity-matrix.md` | Mark `bridge --review-only` partial |
| `docs/superpowers/plans/2026-08-10-host-agent-roadmap.md` | Plan 2 status |
| `README.md` | Document `bridge --review-only` |

---

### Task 1: Minimal git helpers

**Files:**
- Create: `src/git.ts`
- Test: `src/test/git.test.ts`

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

```typescript
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
  getStagedDiffSnapshot,
  hasWorkingTreeChanges,
  resolveGitRepositoryRoot,
  stageAllChanges
} from "../git";

function initRepo(): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-git-"));
  execFileSync("git", ["init"], { cwd: dir });
  execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir });
  execFileSync("git", ["config", "user.name", "Test"], { cwd: dir });
  return dir;
}

test("resolveGitRepositoryRoot returns toplevel", async () => {
  const dir = initRepo();
  const nested = path.join(dir, "sub");
  fs.mkdirSync(nested);
  const root = await resolveGitRepositoryRoot(nested);
  assert.equal(path.resolve(root), path.resolve(dir));
});

test("getStagedDiffSnapshot reads staged files", async () => {
  const dir = initRepo();
  fs.writeFileSync(path.join(dir, "a.txt"), "hello\n", "utf8");
  execFileSync("git", ["add", "a.txt"], { cwd: dir });
  const snap = await getStagedDiffSnapshot(dir);
  assert.match(snap.diff, /hello/);
  assert.deepEqual(snap.changedFiles, ["a.txt"]);
  assert.ok(snap.fullDiffChars > 0);
});

test("stageAllChanges stages working tree files", async () => {
  const dir = initRepo();
  fs.writeFileSync(path.join(dir, "b.txt"), "world\n", "utf8");
  assert.equal(await hasWorkingTreeChanges(dir), true);
  await stageAllChanges(dir);
  const snap = await getStagedDiffSnapshot(dir);
  assert.deepEqual(snap.changedFiles, ["b.txt"]);
});
```

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

Run: `npm test`  
Expected: FAIL — cannot find module `../git` (or similar compile error).

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

Create `src/git.ts` by porting only these symbols from `smart-commit-cli/src/git.ts` (do not copy push/commit/remote helpers):

```typescript
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const GIT_MAX_BUFFER = 10 * 1024 * 1024;

export interface StagedDiffSnapshot {
  diff: string;
  changedFiles: string[];
  fullDiffChars: number;
}

export class GitExecutionError extends Error {
  public constructor(
    message: string,
    public readonly command: string,
    public readonly stderr?: string,
    public readonly stdout?: string
  ) {
    super(message);
    this.name = "GitExecutionError";
  }
}

export async function resolveGitRepositoryRoot(repositoryPath: string): Promise<string> {
  const { stdout } = await runGit(repositoryPath, ["rev-parse", "--show-toplevel"], "git rev-parse --show-toplevel");
  const root = stdout.trim();
  if (!root) {
    throw new GitExecutionError("Git repository root could not be resolved.", "git rev-parse --show-toplevel");
  }
  return root;
}

export async function getStagedDiffSnapshot(repositoryPath: string): Promise<StagedDiffSnapshot> {
  const { stdout } = await runGit(
    repositoryPath,
    ["diff", "--cached", "--no-ext-diff", "--unified=3"],
    "git diff --cached --no-ext-diff --unified=3"
  );
  const changedFilesResult = await runGit(
    repositoryPath,
    ["diff", "--cached", "--name-only", "--diff-filter=ACMR"],
    "git diff --cached --name-only --diff-filter=ACMR"
  );

  return {
    diff: stdout,
    changedFiles: changedFilesResult.stdout
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter((line) => line.length > 0),
    fullDiffChars: stdout.length
  };
}

export async function hasWorkingTreeChanges(repositoryPath: string): Promise<boolean> {
  const { stdout } = await runGit(repositoryPath, ["status", "--porcelain", "-u"], "git status --porcelain -u");
  return stdout.trim().length > 0;
}

export async function stageAllChanges(repositoryPath: string): Promise<void> {
  await runGit(repositoryPath, ["add", "-A"], "git add -A");
}

async function runGit(
  cwd: string,
  args: string[],
  commandLabel: string
): Promise<{ stdout: string; stderr: string }> {
  try {
    const result = await execFileAsync("git", args, { cwd, maxBuffer: GIT_MAX_BUFFER });
    return { stdout: result.stdout, stderr: result.stderr };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    const stderr =
      typeof error === "object" && error !== null && "stderr" in error && typeof error.stderr === "string"
        ? error.stderr
        : undefined;
    const stdout =
      typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string"
        ? error.stdout
        : undefined;
    throw new GitExecutionError(`${commandLabel} failed: ${message}`, commandLabel, stderr, stdout);
  }
}
```

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

Run: `npm test`  
Expected: PASS for new git tests; existing Plan 1 tests still green.

- [ ] **Step 5: Commit**

```bash
git add src/git.ts src/test/git.test.ts
git commit -m "$(cat <<'EOF'
feat: add minimal git helpers for review-only preflight

EOF
)"
```

---

### Task 2: Review parser

**Files:**
- Create: `src/review/types.ts`
- Create: `src/review/parser.ts`
- Test: `src/test/reviewParser.test.ts`

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

```typescript
import assert from "node:assert/strict";
import test from "node:test";
import { parseReviewResponse, shouldBlockReviewResult } from "../review/parser";

test("parseReviewResponse reads score decision summary details", () => {
  const result = parseReviewResponse(
    JSON.stringify({
      score: 8,
      decision: "pass",
      summary: "Looks good",
      details: [{ severity: "P2", message: "nit", filePath: "a.ts", lineNumber: 3 }]
    }),
    "host-agent"
  );
  assert.equal(result.score, 8);
  assert.equal(result.decision, "pass");
  assert.equal(result.summary, "Looks good");
  assert.equal(result.provider, "host-agent");
  assert.equal(result.details.length, 1);
  assert.equal(result.details[0]?.severity, "P2");
});

test("parseReviewResponse accepts fenced JSON", () => {
  const result = parseReviewResponse(
    "```json\n{\"score\":3,\"decision\":\"block\",\"summary\":\"bad\",\"details\":[]}\n```",
    "host-agent"
  );
  assert.equal(result.score, 3);
  assert.equal(result.decision, "block");
});

test("shouldBlockReviewResult uses score vs threshold when score is numeric", () => {
  assert.equal(shouldBlockReviewResult({ decision: "pass", score: 5 }, 6), true);
  assert.equal(shouldBlockReviewResult({ decision: "block", score: 9 }, 6), false);
  assert.equal(shouldBlockReviewResult({ decision: "block", score: null }, 6), true);
  assert.equal(shouldBlockReviewResult({ decision: "pass", score: null }, 6), false);
});
```

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

Run: `npm test`  
Expected: FAIL — missing `../review/parser`.

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

Create `src/review/types.ts`:

```typescript
export type ReviewDecision = "pass" | "block";
export type ReviewSeverity = "P0" | "P1" | "P2" | "P3" | "OTHER";

export interface ReviewDetail {
  severity: ReviewSeverity;
  message: string;
  filePath?: string;
  lineNumber?: number;
}

export interface ReviewExecutionResult {
  decision: ReviewDecision;
  score: number | null;
  summary: string;
  provider: string;
  details: ReviewDetail[];
  raw: string;
}

export interface ReviewExecutionInput {
  repositoryPath: string;
  commitMessage: string | null;
  diff: string;
  changedFiles: string[];
  reviewLanguage: string;
  threshold: number;
}
```

Create `src/review/parser.ts` by porting parse/shouldBlock/extractJson from CLI `src/review/parser.ts` (omit language validators and chunk-prefix helpers if unused). Include at least:

- `parseReviewResponse(raw, provider)`
- `shouldBlockReviewResult(result, threshold)` — **score wins when numeric**: `score <= threshold` → block
- `validateReviewResult(result, threshold)` — structural issues list
- `extractJsonPayload` (fenced or first `{...}`)

Mirror CLI behavior for score null/`"N/A"`, severity fallback to `OTHER`, and default summaries when missing.

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

Run: `npm test`  
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/review/types.ts src/review/parser.ts src/test/reviewParser.test.ts
git commit -m "$(cat <<'EOF'
feat: port review response parser and threshold blocking

EOF
)"
```

---

### Task 3: Review prompt builder

**Files:**
- Create: `src/review/prompt.ts`
- Test: `src/test/reviewPrompt.test.ts`

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

```typescript
import assert from "node:assert/strict";
import test from "node:test";
import { REVIEW_RESPONSE_SCHEMA, buildReviewMessages } from "../review/prompt";

test("buildReviewMessages includes threshold language and diff", () => {
  const messages = buildReviewMessages({
    repositoryPath: "/repo",
    commitMessage: "feat: x",
    diff: "diff --git a/a.ts b/a.ts\n+ok\n",
    changedFiles: ["a.ts"],
    reviewLanguage: "zh-cn",
    threshold: 6
  });
  assert.equal(messages[0]?.role, "system");
  assert.match(messages[0]!.content, /strict JSON/i);
  assert.equal(messages[1]?.role, "user");
  assert.match(messages[1]!.content, /Threshold: 6/);
  assert.match(messages[1]!.content, /Review language: zh-cn/);
  assert.match(messages[1]!.content, /\+ok/);
  assert.match(REVIEW_RESPONSE_SCHEMA, /score/);
});
```

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

Run: `npm test`  
Expected: FAIL — missing prompt module.

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

Create `src/review/prompt.ts` — simplified port of CLI `buildReviewMessages` **without** skills/domain/annotation:

```typescript
import type { HostAgentChatMessage } from "../hostAgent/types";
import type { ReviewExecutionInput } from "./types";

export const REVIEW_RESPONSE_SCHEMA =
  '{"score": number|null, "decision": "pass"|"block", "summary": string, "details": Array<{ "severity": "P0"|"P1"|"P2"|"P3"|"OTHER", "message": string, "filePath"?: string, "lineNumber"?: number }>}' as const;

export function buildReviewMessages(input: ReviewExecutionInput): HostAgentChatMessage[] {
  return [
    {
      role: "system",
      content: [
        "You are a senior code reviewer.",
        "You must obey the requested review language for all natural-language review fields.",
        "Return strict JSON only.",
        `The JSON shape must be: ${REVIEW_RESPONSE_SCHEMA}.`,
        "Use the provided threshold to decide whether the change should pass or block.",
        "When score is numeric, score compared with threshold is the final pass/block standard, so decision must match the score-based outcome.",
        "Use decision alone only when score is null."
      ].join(" ")
    },
    {
      role: "user",
      content: [
        `Repository: ${input.repositoryPath}`,
        `Review language: ${input.reviewLanguage}`,
        `Threshold: ${input.threshold}`,
        `Commit message: ${input.commitMessage ?? "(none)"}`,
        `Changed files (${input.changedFiles.length}):`,
        ...input.changedFiles.map((file) => `- ${file}`),
        "",
        "Staged diff:",
        input.diff
      ].join("\n")
    }
  ];
}
```

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

Run: `npm test`  
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/review/prompt.ts src/test/reviewPrompt.test.ts
git commit -m "$(cat <<'EOF'
feat: add single-stage review prompt for host-agent turns

EOF
)"
```

---

### Task 4: Host-agent review runner

**Files:**
- Create: `src/review/hostAgentReview.ts`
- Test: `src/test/hostAgentReview.test.ts`

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

```typescript
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { createHostAgentClient } from "../hostAgent/client";
import { isNeedsHostAgentError } from "../hostAgent/needsHostAgentError";
import { createSessionStore } from "../hostAgent/sessionStore";
import { runHostAgentReview } from "../review/hostAgentReview";

test("runHostAgentReview throws needs_host_agent then returns blocked after fixture", async () => {
  const base = fs.mkdtempSync(path.join(os.tmpdir(), "scha-rev-"));
  const store = createSessionStore({
    baseDir: base,
    command: "bridge",
    repositoryPath: "/repo",
    cliReferenceVersion: "0.1.21"
  });
  const client = createHostAgentClient({ store });
  const input = {
    repositoryPath: "/repo",
    commitMessage: "Review only",
    diff: "diff --git a/x.ts b/x.ts\n+bad\n",
    changedFiles: ["x.ts"],
    reviewLanguage: "zh-cn",
    threshold: 6
  };

  await assert.rejects(
    () => runHostAgentReview({ client, input }),
    (error: unknown) => isNeedsHostAgentError(error)
  );

  const requestPath = path.join(store.sessionPath, "turns", "0001.request.json");
  const request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
  assert.equal(request.kind, "review");
  assert.equal(request.purpose, "code-review");

  fs.writeFileSync(
    path.join(store.sessionPath, "turns", "0001.response.json"),
    JSON.stringify({
      turnId: "0001",
      content: JSON.stringify({
        score: 2,
        decision: "block",
        summary: "Serious issue",
        details: [{ severity: "P0", message: "bug", filePath: "x.ts" }]
      })
    }),
    "utf8"
  );

  const result = await runHostAgentReview({ client, input });
  assert.equal(result.decision, "block");
  assert.equal(result.score, 2);
  assert.equal(result.provider, "host-agent");
});
```

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

Run: `npm test`  
Expected: FAIL — missing `runHostAgentReview`.

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

```typescript
import type { HostAgentClient } from "../hostAgent/client";
import { parseReviewResponse, shouldBlockReviewResult, validateReviewResult } from "./parser";
import { REVIEW_RESPONSE_SCHEMA, buildReviewMessages } from "./prompt";
import type { ReviewExecutionInput, ReviewExecutionResult } from "./types";

export async function runHostAgentReview(input: {
  client: HostAgentClient;
  input: ReviewExecutionInput;
}): Promise<ReviewExecutionResult> {
  if (!input.input.diff.trim()) {
    throw new Error("Cannot review an empty diff.");
  }

  const messages = buildReviewMessages(input.input);
  const raw = await input.client.review(messages, {
    purpose: "code-review",
    responseSchema: REVIEW_RESPONSE_SCHEMA,
    attempt: 0
  });

  const parsed = parseReviewResponse(raw, "host-agent");
  const issues = validateReviewResult(parsed, input.input.threshold);
  if (issues.length > 0) {
    throw new Error(`Invalid review response: ${issues.join("; ")}`);
  }

  const decision = shouldBlockReviewResult(parsed, input.input.threshold) ? "block" : "pass";
  return { ...parsed, decision };
}
```

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

Run: `npm test`  
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/review/hostAgentReview.ts src/test/hostAgentReview.test.ts
git commit -m "$(cat <<'EOF'
feat: run code-review turns through HostAgentClient

EOF
)"
```

---

### Task 5: Config — `commitMessage.input`

**Files:**
- Modify: `src/config/schema.ts`
- Modify: `src/config/defaults.ts`
- Modify: `src/config/load.ts` (parse `commitMessage.input` if not already)
- Test: extend `src/test/configResolve.test.ts` or add assertion in existing load path

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

Add to `src/test/configResolve.test.ts` (or new file):

```typescript
test("config resolve keeps commitMessage.input from file", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        commitMessage: { input: "feat: provided for review context" }
      }
    }),
    "utf8"
  );
  const result = runCli(["config", "resolve", "--config", configPath, "--output", "json"], {});
  assert.equal(result.exitCode, 0);
  const payload = JSON.parse(result.stdout);
  assert.equal(payload.config.commitMessage.input, "feat: provided for review context");
});
```

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

Run: `npm test`  
Expected: FAIL — `commitMessage.input` undefined / not in schema.

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

1. Add `input: string` to `HostAgentConfig.commitMessage` in `schema.ts`.
2. Default `input: ""` in `defaults.ts`.
3. In `load.ts` `parseCanonicalHostAgentConfig`, parse optional `commitMessage.input` via `parseString` (mirror CLI).

Do not require LLM connection. Do not add `maxDiffChars` for commitMessage unless already present.

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

Run: `npm test`  
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/config/schema.ts src/config/defaults.ts src/config/load.ts src/test/configResolve.test.ts
git commit -m "$(cat <<'EOF'
feat: support commitMessage.input for review-only context

EOF
)"
```

---

### Task 6: `bridge --review-only` command

**Files:**
- Create: `src/commands/bridgeTypes.ts`
- Create: `src/commands/bridge.ts`
- Test: `src/test/bridgeReviewOnly.test.ts` (partial — command-level; full wiring in Task 7)

- [ ] **Step 1: Write the failing test (preflight + blocked fixture helpers can be temporary imports)**

Create `src/test/bridgeReviewOnly.test.ts` with helpers:

```typescript
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { runBridgeCommand } from "../commands/bridge";
import { EXIT_CODE_BLOCKED, EXIT_CODE_NEEDS_HOST_AGENT, EXIT_CODE_SUCCESS } from "../exitCodes";

function initRepoWithStagedChange(): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-bridge-"));
  execFileSync("git", ["init"], { cwd: dir });
  execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir });
  execFileSync("git", ["config", "user.name", "Test"], { cwd: dir });
  fs.writeFileSync(path.join(dir, "app.ts"), "export const x = 1;\n", "utf8");
  execFileSync("git", ["add", "app.ts"], { cwd: dir });
  return dir;
}

test("bridge without --review-only returns config error", async () => {
  const result = await runBridgeCommand(["--repo", "/tmp"], {});
  assert.equal(result.exitCode, 3);
  assert.equal(result.payload.status, "error");
  assert.match(result.payload.error?.message ?? "", /--review-only/);
});

test("bridge --review-only needs host agent then passes with fixture", async () => {
  const repo = initRepoWithStagedChange();
  const sessionBase = fs.mkdtempSync(path.join(os.tmpdir(), "scha-sess-"));

  const first = await runBridgeCommand(
    ["--review-only", "--repo", repo, "--session-base", sessionBase, "--output", "json"],
    {}
  );
  assert.equal(first.exitCode, EXIT_CODE_NEEDS_HOST_AGENT);
  assert.equal(first.payload.status, "needs_host_agent");
  assert.ok(first.payload.sessionPath);
  assert.ok(first.payload.requestPath);
  assert.equal(first.payload.purpose, "code-review");
  assert.equal(first.payload.didCommit, false);
  assert.equal(first.payload.didPush, false);

  fs.writeFileSync(
    path.join(first.payload.sessionPath!, "turns", `${first.payload.turnId}.response.json`),
    JSON.stringify({
      turnId: first.payload.turnId,
      content: JSON.stringify({
        score: 9,
        decision: "pass",
        summary: "OK",
        details: []
      })
    }),
    "utf8"
  );

  const second = await runBridgeCommand(
    ["--review-only", "--repo", repo, "--session", first.payload.sessionPath!, "--output", "json"],
    {}
  );
  assert.equal(second.exitCode, EXIT_CODE_SUCCESS);
  assert.equal(second.payload.status, "passed");
  assert.equal(second.payload.phase, "review");
  assert.equal(second.payload.commitMessageSource, "review-only");
  assert.equal(second.payload.didCommit, false);
  assert.equal(second.payload.didPush, false);
  assert.equal(second.payload.reviewDecision, "pass");
  assert.equal(second.payload.score, 9);
});

test("bridge --review-only blocks when fixture score is at or below threshold", async () => {
  const repo = initRepoWithStagedChange();
  const sessionBase = fs.mkdtempSync(path.join(os.tmpdir(), "scha-sess-"));

  const first = await runBridgeCommand(
    ["--review-only", "--repo", repo, "--session-base", sessionBase, "--output", "json"],
    {}
  );
  assert.equal(first.exitCode, EXIT_CODE_NEEDS_HOST_AGENT);

  fs.writeFileSync(
    path.join(first.payload.sessionPath!, "turns", `${first.payload.turnId}.response.json`),
    JSON.stringify({
      turnId: first.payload.turnId,
      content: JSON.stringify({
        score: 4,
        decision: "block",
        summary: "Too risky",
        details: [{ severity: "P0", message: "security", filePath: "app.ts" }]
      })
    }),
    "utf8"
  );

  const second = await runBridgeCommand(
    ["--review-only", "--repo", repo, "--session", first.payload.sessionPath!, "--output", "json"],
    {}
  );
  assert.equal(second.exitCode, EXIT_CODE_BLOCKED);
  assert.equal(second.payload.status, "blocked");
  assert.equal(second.payload.error?.code, "REVIEW_BLOCKED");
  assert.equal(second.payload.didCommit, false);
});
```

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

Run: `npm test`  
Expected: FAIL — missing `runBridgeCommand`.

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

**`src/commands/bridgeTypes.ts`** — host-agent bridge payload. Include:

- `status`: `"ready" | "passed" | "blocked" | "error" | "needs_host_agent"`
- Common fields aligned with CLI where practical: `command: "bridge"`, `phase`, `repositoryPath`, `didCommit`, `didPush`, `dryRun`, `threshold`, `score`, `reviewSummary`, `reviewDecision`, `reviewProvider`, `reviewDetails`, `commitMessage`, `commitMessageSource`, `summary`, `error`
- For `needs_host_agent`: `sessionPath`, `requestPath`, `turnId`, `purpose`
- Stub: `passHistory` / `stagedChangeSummary` / `pullRequestCreation` as disabled/skipped defaults
- `schemaVersion`: use `"smart-commit-host-agent.bridge.v1"` (do not claim CLI schema id)

**`src/commands/bridge.ts`** — behavior checklist:

1. Parse flags: `--review-only` (required for Plan 2), `--dry-run`, `--repo`, `--session`, `--session-base`, `--output`, `--config`, `--commit-message`.
2. If missing `--review-only` → exit `3`, `CONFIG_ERROR`, message like `Plan 2 only supports bridge --review-only; full bridge lands in a later plan.`
3. Resolve config via `resolveHostAgentConfig` (no LLM). Force `git.autoCommit=false`, `git.autoPush=false` for the run (review-only).
4. Require `--repo`; resolve git root; load staged diff; auto-stage if `autoStageWhenNothingStaged` and working tree dirty (same block codes as CLI: `NO_CHANGES`, `NO_STAGED_DIFF`, `WOULD_AUTO_STAGE` on dry-run).
5. Truncate diff with helper `truncateDiffForHostAgent(diff, maxDiffChars)` (port CLI `truncateDiffForLlm`, rename note string to `smart-commit-host-agent`).
6. Commit message for review context: `config.commitMessage.input` or `--commit-message` flag, else `"Review only run (no commit message provided)."`; source always `"review-only"`.
7. `--dry-run` → `status: "ready"`, phase `preflight`, exit `0` (no turn).
8. Open/create session (`command: "bridge"`), create `HostAgentClient`, call `runHostAgentReview`.
9. Catch `NeedsHostAgentError` → exit `10`, payload `needs_host_agent` (+ still include `didCommit: false`, `didPush: false`, `command: "bridge"`).
10. On review result: apply `shouldBlockReviewResult` already done in runner; map block → exit `2` + `REVIEW_BLOCKED`; pass → exit `0` with summary `"Review-only run passed. Commit-message handling, local commit, and push were skipped."`
11. Never call git commit/push.

Also export `runBridgeCommand(argv, env): Promise<{ exitCode: number; payload: BridgeOutput; stdout?: string; stderr?: string }>` that stringifies JSON for stdout when used from CLI (or return payload and let cliApp render — match probe style: command returns `{ exitCode, stdout, stderr }`).

Recommended: match `runHostAgentProbeCommand` shape:

```typescript
export interface BridgeCommandResult {
  exitCode: number;
  stdout: string;
  stderr: string;
}
```

Keep `payload` accessible in tests either by exporting a test helper `runBridgeCommandForTest` that returns payload, or parse stdout JSON in tests. Prefer **returning payload on a dual API**:

```typescript
export async function runBridgeCommand(...): Promise<{ exitCode: number; stdout: string; stderr: string; payload: BridgeOutput }>
```

Update the Task 6 tests above to use `result.payload` accordingly.

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

Run: `npm test`  
Expected: PASS for bridge review-only tests.

- [ ] **Step 5: Commit**

```bash
git add src/commands/bridgeTypes.ts src/commands/bridge.ts src/test/bridgeReviewOnly.test.ts
git commit -m "$(cat <<'EOF'
feat: implement bridge --review-only with host-agent review turns

EOF
)"
```

---

### Task 7: Wire CLI entry

**Files:**
- Modify: `src/cliApp.ts`
- Modify: `src/test/cliApp.test.ts` (help lists bridge; async dispatch)
- Modify: `README.md` (usage snippet)

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

In `src/test/cliApp.test.ts`:

```typescript
test("parseCliCommand classifies bridge", () => {
  const command = parseCliCommand(["bridge", "--review-only", "--repo", "."]);
  assert.equal(command.kind, "bridge");
});

test("runCli rejects bridge synchronously like other async commands", () => {
  assert.throws(
    () => runCli(["bridge", "--review-only", "--repo", "."], {}),
    /must be handled asynchronously/
  );
});

test("help mentions bridge --review-only", () => {
  const result = runCli(["--help"], {});
  assert.match(result.stdout, /bridge/);
  assert.match(result.stdout, /review-only/);
});
```

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

Run: `npm test`  
Expected: FAIL — `bridge` unknown / help missing.

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

Update `ParsedCommand` with `{ kind: "bridge"; raw: string; flags: string[] }`.

In `parseCliCommand`, when `positional[0] === "bridge"`, return bridge with `filterCommandTokens(argv, 1)`.

In `runCli`, throw for `bridge` (same pattern as probe).

In `runCliAsync`, dispatch `runBridgeCommand(command.flags, env)` when kind is bridge (and still allow sync commands via `runCli`).

Update `buildHelpText()`:

```text
  smart-commit-host-agent bridge --review-only --repo <path> [flags]

Plan 2 commands:
  bridge --review-only   Local code review via host-agent turns (no commit/push)
```

Flags to list: `--repo`, `--session`, `--session-base`, `--dry-run`, `--commit-message`, `--config`, `--output`.

Update README with a short Plan 2 section showing needs_host_agent → write response → resume loop (mirror probe docs).

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

Run: `npm test`  
Expected: all green.

- [ ] **Step 5: Commit**

```bash
git add src/cliApp.ts src/test/cliApp.test.ts README.md
git commit -m "$(cat <<'EOF'
feat: expose bridge --review-only on the CLI

EOF
)"
```

---

### Task 8: Docs + matrix + roadmap

**Files:**
- Modify: `docs/parity-matrix.md`
- Modify: `docs/superpowers/plans/2026-08-10-host-agent-roadmap.md`
- Modify: `CHANGELOG.md` (Unreleased / Plan 2 notes — do not bump version unless releasing)

- [ ] **Step 1: Update parity matrix**

Set:

| `bridge --review-only` | Plan 2 部分对齐 | 单次审查 turn；无 commit/push；无 chunked |
| `bridge`（完整） | 未跟进 | Plan 3 |

Bump matrix host-agent status note to “Plan 2 in progress / complete” without claiming a release unless packaging.

- [ ] **Step 2: Update roadmap current execution**

```markdown
## 当前执行

Plan 2 已完成（`bridge --review-only`）；下一步写 Plan 3 详细任务再继续。

## Plan 2 status: complete

- **Date:** 2026-08-10
- **验收:** fixture response 跑通 `needs_host_agent` → `passed` / `blocked`；不 commit/push
```

- [ ] **Step 3: CHANGELOG Unreleased**

Add bullet under Unreleased: `bridge --review-only` host-agent review loop.

- [ ] **Step 4: Commit**

```bash
git add docs/parity-matrix.md docs/superpowers/plans/2026-08-10-host-agent-roadmap.md CHANGELOG.md
git commit -m "$(cat <<'EOF'
docs: mark Plan 2 bridge --review-only complete in matrix

EOF
)"
```

---

### Task 9: Final verification

- [ ] **Step 1: Run full test suite**

Run: `npm test`  
Expected: all tests pass (Plan 1 + Plan 2).

- [ ] **Step 2: Manual smoke (optional but recommended)**

In a throwaway git repo with a staged file:

```bash
npm run build
node out/cli.js bridge --review-only --repo "$REPO" --session-base /tmp/scha --output json
# write fixture response for turn
node out/cli.js bridge --review-only --repo "$REPO" --session "$SESSION" --output json
```

Expected: first exit 10 / `needs_host_agent`; second `passed` or `blocked` per fixture; `didCommit`/`didPush` false.

- [ ] **Step 3: Confirm no LLM HTTP**

Grep workspace for `openai|anthropic|baseUrl|apiKey` usage in new Plan 2 code paths — only config rejection of LLM flags should remain; review must go through `HostAgentClient` only.

---

## Self-review (plan vs spec)

| Spec / roadmap requirement | Task |
|----------------------------|------|
| `bridge --review-only` via turn | 4, 6, 7 |
| No LLM connection / no HTTP LLM | 4, 6, 9 |
| `needs_host_agent` + `--session` resume | 6, 7 |
| Parse/score → `passed` / `blocked` | 2, 4, 6 |
| No commit/push | 6 |
| Fixture integration test | 6 |
| Matrix / roadmap update | 8 |
| Chunked review / full bridge / skill update | Explicitly deferred (Plan 3–5) |
