# Review-skill CLI Parity 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:** Align host-agent `review.skill` (id/path/promptTuning), bundled code-review skills, domain classifier, PR line-number annotation/rules, and `detailLocator` with `smart-commit-cli` 0.1.21, without chunked review, repair turns, or CLI flag/env overlays.

**Architecture:** Surgical port into existing `src/config/*`, `src/promptContext.ts`, and `src/review/*`. Copy CLI `code-review-skills`, `diffClassifier.ts`, `skills/types.ts`, and `detailLocator.ts`. Inject skill guidance in the review **user** message. PR/MR review sets `inlineAnchoring: true`; staged `bridge` does not. Keep a single review turn.

**Tech Stack:** TypeScript 5.6, Node 20+, `node:test`, existing HostAgentClient / SessionStore.

**Spec:** `docs/superpowers/specs/2026-08-14-review-skill-cli-parity-design.md`  
**Reference CLI (read-only):** `/Users/nietao/VSCode-plugins/smart-commit-cli` @ package `peerReference.cliVersion`

**Global constraints:**
- Do not modify `smart-commit-cli`
- Zero LLM HTTP; no correction repair turns; no chunked / hybrid review
- Do not add `--code-review-skill-*` / `SMART_COMMIT_REVIEW_*`
- Do not tighten `review.language` to `OutputLanguage`
- Do not port `validateReviewContentLanguage`
- Do not write passHistory
- Do not change `review.maxDiffChars` default (`200000`)

---

## File structure

| Path | Responsibility |
|------|----------------|
| `src/config/constants.ts` | Extend `BuiltinSkillCategory` and `builtinSkillIds` with `code-review` ids |
| `src/config/schema.ts` | `review.skill`; `pullRequestReview.skillPromptTuning`; validate builtin id when `path` is empty |
| `src/config/defaults.ts` | Defaults: skill `code-review`/`""`/`""`; `skillPromptTuning` `""` |
| `src/config/load.ts` | Parse `review.skill` via existing `parseSkillConfig`; parse `skillPromptTuning`; nested `review.skill` merge |
| `src/promptContext.ts` | Load `code-review-skills` as well as `git-commit-message-skills` |
| `src/code-review-skills/**` | Copied bundled review skills |
| `src/skills/types.ts` | Copied `ReviewSkill` / `ReviewSkillDomain` |
| `src/review/diffClassifier.ts` | Copied domain classifier |
| `src/review/detailLocator.ts` | Copied `alignReviewResultWithDiff` |
| `src/review/types.ts` | Optional `skillPromptTuning`, `annotateLineNumbers`, `inlineAnchoring` |
| `src/review/prompt.ts` | CLI single-stage `buildReviewMessages` (skill + domain + optional line rules). No chunk/repair/summary builders |
| `src/review/hostAgentReview.ts` | Take `config`; build augmentation; annotate via prompt flags; locate against raw diff |
| `src/commands/bridge.ts` | Pass `config`; do not set `inlineAnchoring` |
| `src/commands/pullRequestReview.ts` | `inlineAnchoring: true` + `skillPromptTuning` override |
| `src/commands/myPullRequestBatchReview.ts` | Same as pull-request review |
| `src/contracts.ts` | Config schema for `review.skill` and `skillPromptTuning` |
| `package.json` | Ship `src/code-review-skills` |
| Tests | `configResolve`, `promptContext`, `diffClassifier`, `detailLocator`, `reviewPrompt`, `hostAgentReview` |
| Docs | configuration, parity-matrix, README, example config |

---

### Task 1: Config contract (types, defaults, parse, validate)

**Files:**
- Modify: `src/config/constants.ts`, `src/config/schema.ts`, `src/config/defaults.ts`, `src/config/load.ts`
- Test: `src/test/configResolve.test.ts`

- [ ] **Step 1: Write failing config tests** at the end of `src/test/configResolve.test.ts`

```ts
test("review.skill defaults match CLI (code-review, empty path and promptTuning)", () => {
  const resolved = resolveHostAgentConfig({ env: {} });
  assert.equal(resolved.config.review.skill.id, "code-review");
  assert.equal(resolved.config.review.skill.path, "");
  assert.equal(resolved.config.review.skill.promptTuning, "");
  assert.equal(resolved.config.pullRequestReview.skillPromptTuning, "");
});

test("review.skill nested merge keeps default path and promptTuning", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        review: { skill: { id: "java-code-review" } }
      }
    }),
    "utf8"
  );
  const resolved = resolveHostAgentConfig({ configPath, env: {} });
  assert.equal(resolved.config.review.skill.id, "java-code-review");
  assert.equal(resolved.config.review.skill.path, "");
  assert.equal(resolved.config.review.skill.promptTuning, "");
});

test("review.skill.id must be builtin when path is empty", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        review: { skill: { id: "not-a-skill" } }
      }
    }),
    "utf8"
  );
  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {} }),
    /review\.skill\.id must be one of: code-review, frontend-code-review/
  );
});

test("review.skill custom path skips builtin id check", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        review: { skill: { id: "not-a-skill", path: "review-skill.txt" } }
      }
    }),
    "utf8"
  );
  const resolved = resolveHostAgentConfig({ configPath, env: {} });
  assert.equal(resolved.config.review.skill.id, "not-a-skill");
  assert.equal(resolved.config.review.skill.path, "review-skill.txt");
});

test("review.skill.promptTuning and pullRequestReview.skillPromptTuning are trimmed", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        review: { skill: { promptTuning: "  ignore logging wrapper  " } },
        pullRequestReview: { skillPromptTuning: "  review like a senior engineer  " }
      }
    }),
    "utf8"
  );
  const resolved = resolveHostAgentConfig({ configPath, env: {} });
  assert.equal(resolved.config.review.skill.promptTuning, "ignore logging wrapper");
  assert.equal(resolved.config.pullRequestReview.skillPromptTuning, "review like a senior engineer");
});
```

- [ ] **Step 2: Run the new tests; expect FAIL**

Run: `npm test -- --test-name-pattern="review.skill"`

Expected: FAIL (e.g. `skill` undefined on `review`).

- [ ] **Step 3: Implement config**

In `src/config/constants.ts`, change:

```ts
export type BuiltinSkillCategory = "code-review" | "git-commit-message";

export const builtinSkillIds: Record<BuiltinSkillCategory, readonly string[]> = {
  "code-review": [
    "code-review",
    "frontend-code-review",
    "mobile-code-review",
    "python-code-review",
    "golang-code-review",
    "java-code-review",
    "c-code-review",
    "cpp-code-review",
    "csharp-code-review",
    "rust-code-review",
    "php-code-review"
  ],
  "git-commit-message": ["conventional", "semantic", "gitmoji"]
};
```

In `src/config/schema.ts` `HostAgentConfig.review` add:

```ts
skill: {
  id: string;
  path: string;
  promptTuning: string;
};
```

In `HostAgentConfig.pullRequestReview` add `skillPromptTuning: string;` after `commentSeverities`.

In `validateHostAgentConfig`, after the commit-message skill check:

```ts
if (!config.review.skill.path) {
  resolveBuiltinSkillId(config.review.skill.id, "code-review", "review.skill.id");
}
```

In `src/config/defaults.ts`:

```ts
review: {
  threshold: 6,
  language: "zh-cn",
  maxDiffChars: 200_000,
  skill: {
    id: "code-review",
    path: "",
    promptTuning: ""
  }
},
```

and in `pullRequestReview`:

```ts
skillPromptTuning: "",
```

(keep existing pullRequestReview fields; insert `skillPromptTuning` after `commentSeverities` to match CLI order).

In `src/config/load.ts` `parseCanonicalHostAgentConfig` review block, add:

```ts
...(Object.hasOwn(review, "skill")
  ? { skill: parseSkillConfig(review.skill, `${sourceLabel}.review.skill`) }
  : {})
```

In the `pullRequestReview` block, add:

```ts
...(Object.hasOwn(pullRequestReview, "skillPromptTuning")
  ? {
      skillPromptTuning: parseString(
        pullRequestReview.skillPromptTuning,
        `${sourceLabel}.pullRequestReview.skillPromptTuning`
      ).trim()
    }
  : {}),
```

Change `mergeHostAgentConfig` review line from shallow spread to:

```ts
review: {
  ...base.review,
  ...override.review,
  skill: {
    ...base.review.skill,
    ...override.review?.skill
  }
},
```

Keep `parseSkillConfig` as-is (same `{id,path,promptTuning}` shape). Optionally retarget its return type to `HostAgentConfig["review"]["skill"]`; both are identical.

- [ ] **Step 4: Re-run tests; expect PASS**

Run: `npm test -- --test-name-pattern="review.skill|commitMessage.skill"`

Expected: PASS. Existing commit-message skill tests still pass.

- [ ] **Step 5: Commit**

```bash
git add src/config/constants.ts src/config/schema.ts src/config/defaults.ts src/config/load.ts src/test/configResolve.test.ts
git commit -m "feat: parse review.skill and pullRequestReview.skillPromptTuning like CLI"
```

---

### Task 2: Copy bundled code-review skills and load them in promptContext

**Files:**
- Create: `src/code-review-skills/**` (copy from CLI)
- Modify: `src/promptContext.ts`, `package.json`
- Test: `src/test/promptContext.test.ts`

- [ ] **Step 1: Copy skill files**

```bash
mkdir -p src/code-review-skills
cp -R /Users/nietao/VSCode-plugins/smart-commit-cli/src/code-review-skills/. src/code-review-skills/
```

Confirm these exist:

- `src/code-review-skills/code-review/SKILL.md`
- `src/code-review-skills/code-review/references/scoring-guide.md`
- `src/code-review-skills/c-code-review/SKILL.md`
- `src/code-review-skills/java-code-review/SKILL.md`
- and the other 8 skill directories (`frontend`, `mobile`, `python`, `golang`, `cpp`, `csharp`, `rust`, `php`)

Add `"src/code-review-skills"` next to `"src/git-commit-message-skills"` in `package.json` `files`.

- [ ] **Step 2: Write failing tests** in `src/test/promptContext.test.ts` (keep existing commit-message tests)

```ts
test("buildPromptAugmentation loads bundled code-review skill", () => {
  const lines = buildPromptAugmentation({
    repositoryPath: "/repo",
    skill: { id: "code-review", path: "", promptTuning: "" },
    categoryLabel: "review skill",
    builtinCategory: "code-review"
  });
  const rendered = lines.join("\n\n");
  assert.match(rendered, /Selected review skill profile: code-review\./);
  assert.match(rendered, /Bundled review skill instructions:/);
  assert.match(rendered, /Bundled review skill reference \(scoring-guide\.md\):/);
});

test("buildPromptAugmentation loads bundled c-code-review skill instead of generic code-review", () => {
  const lines = buildPromptAugmentation({
    repositoryPath: "/repo",
    skill: { id: "c-code-review", path: "", promptTuning: "" },
    categoryLabel: "review skill",
    builtinCategory: "code-review"
  });
  const rendered = lines.join("\n\n");
  assert.match(rendered, /Selected review skill profile: c-code-review\./);
  assert.match(rendered, /# C 代码审查/);
  assert.doesNotMatch(rendered, /Selected review skill profile: code-review\./);
});

test("buildPromptAugmentation loads custom review skill file relative to repositoryPath and promptTuning", () => {
  const repositoryPath = fs.mkdtempSync(path.join(os.tmpdir(), "scha-review-skill-"));
  fs.mkdirSync(path.join(repositoryPath, ".smart-commit"));
  fs.writeFileSync(
    path.join(repositoryPath, ".smart-commit", "review-skill.txt"),
    "Prefer concise risk summaries.",
    "utf8"
  );
  const lines = buildPromptAugmentation({
    repositoryPath,
    skill: {
      id: "ignored",
      path: ".smart-commit/review-skill.txt",
      promptTuning: "Ignore findings that only complain about the project's required logging wrapper."
    },
    categoryLabel: "review skill",
    builtinCategory: "code-review"
  });
  assert.match(lines[0] ?? "", /Prefer concise risk summaries/);
  assert.match(lines[1] ?? "", /Ignore findings that only complain about the project's required logging wrapper/);
  assert.doesNotMatch(lines.join("\n"), /Selected review skill profile/);
});

test("buildPromptAugmentation rejects empty custom review skill file", () => {
  const repositoryPath = fs.mkdtempSync(path.join(os.tmpdir(), "scha-review-skill-empty-"));
  fs.writeFileSync(path.join(repositoryPath, "empty.txt"), "   \n", "utf8");
  assert.throws(
    () =>
      buildPromptAugmentation({
        repositoryPath,
        skill: { id: "ignored", path: "empty.txt", promptTuning: "" },
        categoryLabel: "review skill",
        builtinCategory: "code-review"
      }),
    /review skill skill file must not be empty/
  );
});
```

- [ ] **Step 3: Run tests; expect FAIL**

Run: `npm test -- --test-name-pattern="buildPromptAugmentation loads bundled code-review|loads bundled c-code-review|custom review skill|empty custom review"`

Expected: FAIL (code-review category currently returns null / only profile line, no bundle).

- [ ] **Step 4: Restore dual-category loading in `src/promptContext.ts`**

Replace `loadBuiltinSkillPromptBundle` with the CLI version:

```ts
function loadBuiltinSkillPromptBundle(
  builtinCategory: BuiltinSkillCategory | undefined,
  skillId: string
): { instructions: string; references: Array<{ fileName: string; content: string }> } | null {
  const rootDirectoryName =
    builtinCategory === "git-commit-message"
      ? "git-commit-message-skills"
      : builtinCategory === "code-review"
        ? "code-review-skills"
        : null;

  if (rootDirectoryName === null) {
    return null;
  }

  const skillDirectory = resolveBundledSkillDirectory(rootDirectoryName, skillId);
  if (!skillDirectory) {
    return null;
  }

  const skillFilePath = path.join(skillDirectory, "SKILL.md");
  if (!fs.existsSync(skillFilePath) || !fs.statSync(skillFilePath).isFile()) {
    return null;
  }

  const instructions = stripMarkdownFrontmatter(fs.readFileSync(skillFilePath, "utf8"));
  if (!instructions) {
    return null;
  }

  return {
    instructions,
    references: loadBundledSkillReferences(skillDirectory)
  };
}
```

Do not change `resolveSkillPath` (already matches CLI: absolute `normalize`, relative to `repositoryPath`).

- [ ] **Step 5: `npm test` green; commit**

Run: `npm test`

Expected: PASS (including existing commit-message promptContext tests).

```bash
git add src/code-review-skills src/promptContext.ts src/test/promptContext.test.ts package.json
git commit -m "feat: bundle code-review skills and load them through promptContext"
```

---

### Task 3: Copy domain classifier

**Files:**
- Create: `src/skills/types.ts`, `src/review/diffClassifier.ts`, `src/test/diffClassifier.test.ts`

- [ ] **Step 1: Copy source and tests from CLI**

```bash
mkdir -p src/skills
cp /Users/nietao/VSCode-plugins/smart-commit-cli/src/skills/types.ts src/skills/types.ts
cp /Users/nietao/VSCode-plugins/smart-commit-cli/src/review/diffClassifier.ts src/review/diffClassifier.ts
cp /Users/nietao/VSCode-plugins/smart-commit-cli/src/test/diffClassifier.test.ts src/test/diffClassifier.test.ts
```

Do not edit the copied classifier or its tests. Import paths in the CLI test (`../review/diffClassifier`, `../skills/types`) already match this layout.

- [ ] **Step 2: Run classifier tests**

Run: `npm test -- --test-name-pattern="detectDiffDomain|analyzeReviewSkillDomain"`

Expected: PASS. If TypeScript fails, only fix host-agent compile issues (do not “simplify” classifier logic).

- [ ] **Step 3: Commit**

```bash
git add src/skills/types.ts src/review/diffClassifier.ts src/test/diffClassifier.test.ts
git commit -m "feat: port CLI review domain classifier"
```

---

### Task 4: Port single-stage review prompt (skill, domain, line rules)

**Files:**
- Modify: `src/review/types.ts`, `src/review/prompt.ts`
- Test: `src/test/reviewPrompt.test.ts`

- [ ] **Step 1: Extend `ReviewExecutionInput` in `src/review/types.ts`**

Add these optional fields (CLI names):

```ts
  skillPromptTuning?: string;
  annotateLineNumbers?: boolean;
  inlineAnchoring?: boolean;
```

- [ ] **Step 2: Replace `src/test/reviewPrompt.test.ts` with:**

```ts
import assert from "node:assert/strict";
import test from "node:test";
import { REVIEW_RESPONSE_SCHEMA, buildReviewMessages } from "../review/prompt";
import type { ReviewExecutionInput } from "../review/types";

function sampleInput(overrides: Partial<ReviewExecutionInput> = {}): ReviewExecutionInput {
  return {
    repositoryPath: "/repo",
    commitMessage: "feat: x",
    diff: "diff --git a/a.ts b/a.ts\n--- a/a.ts\n+++ b/a.ts\n@@ -1,0 +1,1 @@\n+ok\n",
    changedFiles: ["a.ts"],
    reviewLanguage: "zh-cn",
    threshold: 6,
    ...overrides
  };
}

test("buildReviewMessages includes threshold language and diff", () => {
  const messages = buildReviewMessages(sampleInput());
  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/);
});

test("buildReviewMessages puts skill guidance and detected domain in the user message", () => {
  const messages = buildReviewMessages(sampleInput(), {
    promptAugmentation: ["Selected review skill profile: code-review."],
    selectedSkillId: "code-review",
    selectedSkillPath: ""
  });
  assert.doesNotMatch(messages[0]!.content, /Review skill guidance/);
  assert.match(messages[1]!.content, /Review skill guidance:/);
  assert.match(messages[1]!.content, /Selected review skill profile: code-review\./);
  assert.match(messages[1]!.content, /Detected diff domain:/);
  assert.match(messages[1]!.content, /fallback to generic review:/);
});

test("buildReviewMessages treats a custom skill path as generic domain", () => {
  const messages = buildReviewMessages(sampleInput(), {
    selectedSkillId: "java-code-review",
    selectedSkillPath: "review-skill.txt"
  });
  assert.match(messages[1]!.content, /selected skill domain: generic/);
});

test("buildReviewMessages does not annotate line numbers without inlineAnchoring", () => {
  const messages = buildReviewMessages(sampleInput());
  assert.match(messages[1]!.content, /^Staged diff:$/m);
  assert.doesNotMatch(messages[1]!.content, /\| \+ok/);
  assert.doesNotMatch(messages[1]!.content, /Line-number citation/);
  assert.doesNotMatch(messages[1]!.content, /Per-line findings/);
});

test("buildReviewMessages annotates new-file line numbers when inlineAnchoring is true", () => {
  const messages = buildReviewMessages(sampleInput({ inlineAnchoring: true }));
  assert.match(messages[1]!.content, /Staged diff \(each body line is prefixed with its new-file line number/);
  assert.match(messages[1]!.content, /1 \| \+ok/);
  assert.match(messages[1]!.content, /Line-number citation/);
  assert.match(messages[1]!.content, /Per-line findings/);
});
```

- [ ] **Step 3: Run tests; expect FAIL**

Run: `npm test -- --test-name-pattern="buildReviewMessages"`

Expected: FAIL (`buildReviewMessages` still takes one argument; no skill/domain/annotation).

- [ ] **Step 4: Port CLI single-stage prompt into `src/review/prompt.ts`**

Keep `REVIEW_RESPONSE_SCHEMA`. Change `buildReviewMessages` to `(input, options?)` returning `HostAgentChatMessage[]`.

Copy from CLI `src/review/prompt.ts` **only**:

- `ReviewPromptOptions`
- `buildReviewMessages` (return type: `HostAgentChatMessage[]`)
- `buildReviewPromptAugmentationSection`
- `buildReviewRuleSection`, `buildLineNumberCitationRules`, `buildPerLineFindingRules`
- `shouldAnnotateLineNumbers`, `buildDiffSectionHeading`
- `annotateDiffWithLineNumbers`, `formatNewLineNumber`
- `buildReviewDiffDomainSection`, `buildReviewSkillForDetection`, `resolveSelectedReviewSkillDomain`

Do **not** copy `buildChunkReviewMessages`, `buildSummaryReviewMessages`, `buildReviewRepairMessages`, or `ReviewChunkMeta`.

Imports:

```ts
import type { HostAgentChatMessage } from "../hostAgent/types";
import type { ReviewSkill, ReviewSkillDomain } from "../skills/types";
import { analyzeReviewSkillDomain } from "./diffClassifier";
import type { ReviewExecutionInput } from "./types";
```

Use existing `REVIEW_RESPONSE_SCHEMA` in the system string via template, as the current host-agent file does.

The user-message order must stay: metadata → skill guidance → detected domain → staged diff heading/body → optional rules.

- [ ] **Step 5: Re-run prompt tests; commit**

Run: `npm test -- --test-name-pattern="buildReviewMessages"`

Expected: PASS.

```bash
git add src/review/types.ts src/review/prompt.ts src/test/reviewPrompt.test.ts
git commit -m "feat: inject review skill, domain classification, and PR line rules into prompt"
```

---

### Task 5: Port detailLocator

**Files:**
- Create: `src/review/detailLocator.ts`, `src/test/detailLocator.test.ts`

- [ ] **Step 1: Copy from CLI**

```bash
cp /Users/nietao/VSCode-plugins/smart-commit-cli/src/review/detailLocator.ts src/review/detailLocator.ts
cp /Users/nietao/VSCode-plugins/smart-commit-cli/src/test/detailLocator.test.ts src/test/detailLocator.test.ts
```

`detailLocator.ts` imports `stripReviewChunkPrefix` from `./parser` — host-agent already exports that. Do not rewrite locator logic.

- [ ] **Step 2: Run locator tests**

Run: `npm test -- --test-name-pattern="alignReviewResultWithDiff"`

Expected: PASS.

- [ ] **Step 3: Commit**

```bash
git add src/review/detailLocator.ts src/test/detailLocator.test.ts
git commit -m "feat: port CLI review detail locator"
```

---

### Task 6: Wire runHostAgentReview and command call sites

**Files:**
- Modify: `src/review/hostAgentReview.ts`
- Modify: `src/commands/bridge.ts` (both `runHostAgentReview` calls, ~263 and ~762)
- Modify: `src/commands/pullRequestReview.ts` (~202)
- Modify: `src/commands/myPullRequestBatchReview.ts` (~512)
- Test: `src/test/hostAgentReview.test.ts`

- [ ] **Step 1: Update `src/test/hostAgentReview.test.ts`**

```ts
import { createDefaultHostAgentConfig } from "../config/defaults";

// inside the existing test, after creating `input`:
const config = createDefaultHostAgentConfig();

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

// after writing the fixture response:
const result = await runHostAgentReview({ client, config, input });

const request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
assert.match(JSON.stringify(request), /Review skill guidance/);
assert.match(JSON.stringify(request), /Detected diff domain:/);
assert.doesNotMatch(JSON.stringify(request), /Line-number citation/);
```

Add a second test:

```ts
test("runHostAgentReview uses skillPromptTuning override and inlineAnchoring for PR-style input", async () => {
  const base = fs.mkdtempSync(path.join(os.tmpdir(), "scha-rev-pr-"));
  const store = createSessionStore({
    baseDir: base,
    command: "pull-request-review",
    repositoryPath: "/repo",
    cliReferenceVersion: "0.1.21"
  });
  const client = createHostAgentClient({ store });
  const config = createDefaultHostAgentConfig();
  config.review.skill.promptTuning = "staged tuning";
  const input = {
    repositoryPath: "/repo",
    commitMessage: "Reviewing MR #1: title",
    diff: "diff --git a/x.ts b/x.ts\n--- a/x.ts\n+++ b/x.ts\n@@ -1,0 +1,1 @@\n+bad\n",
    changedFiles: ["x.ts"],
    reviewLanguage: "zh-cn",
    threshold: 6,
    skillPromptTuning: "pr tuning",
    inlineAnchoring: true
  };

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

  const request = JSON.parse(fs.readFileSync(path.join(store.sessionPath, "turns", "0001.request.json"), "utf8"));
  const payload = JSON.stringify(request);
  assert.match(payload, /Additional review skill tuning:\npr tuning/);
  assert.doesNotMatch(payload, /staged tuning/);
  assert.match(payload, /Line-number citation/);
  assert.match(payload, /1 \| \+bad/);
});
```

- [ ] **Step 2: Run; expect FAIL (compile: `config` missing)**

Run: `npm test -- --test-name-pattern="runHostAgentReview"`

Expected: FAIL.

- [ ] **Step 3: Implement `src/review/hostAgentReview.ts`**

```ts
import type { HostAgentConfig } from "../config/schema";
import type { HostAgentClient } from "../hostAgent/client";
import { buildPromptAugmentation } from "../promptContext";
import { alignReviewResultWithDiff } from "./detailLocator";
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;
  config: HostAgentConfig;
  input: ReviewExecutionInput;
  purpose?: string;
}): Promise<ReviewExecutionResult> {
  if (!input.input.diff.trim()) {
    throw new Error("Cannot review an empty diff.");
  }

  const messages = buildReviewMessages(input.input, {
    promptAugmentation: buildPromptAugmentation({
      repositoryPath: input.input.repositoryPath,
      skill: {
        ...input.config.review.skill,
        promptTuning: input.input.skillPromptTuning ?? input.config.review.skill.promptTuning
      },
      categoryLabel: "review skill",
      builtinCategory: "code-review"
    }),
    selectedSkillId: input.config.review.skill.id,
    selectedSkillPath: input.config.review.skill.path
  });
  const raw = await input.client.review(messages, {
    purpose: input.purpose ?? "code-review",
    responseSchema: REVIEW_RESPONSE_SCHEMA,
    attempt: 0
  });

  const parsed = alignReviewResultWithDiff(parseReviewResponse(raw, "host-agent"), input.input.diff);
  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 };
}
```

Locator must receive the **raw** `input.input.diff`, not the annotated prompt copy.

- [ ] **Step 4: Update call sites**

`src/commands/bridge.ts` both calls (full bridge ~263 and `--review-only` ~762):

```ts
const review = await runHostAgentReview({
  client,
  config,
  input: {
    repositoryPath,
    commitMessage: state.commitMessage, // or commitMessageInfo.message in review-only
    diff: reviewDiff,
    changedFiles,
    reviewLanguage: config.review.language,
    threshold: config.review.threshold
  }
});
```

Do **not** set `inlineAnchoring` or `skillPromptTuning` here.

`src/commands/pullRequestReview.ts` (~202):

```ts
const review = await runHostAgentReview({
  client,
  config,
  input: {
    repositoryPath,
    commitMessage: `Reviewing ${itemLabel} #${source.number}: ${source.title || "(no title)"}`,
    diff,
    changedFiles: [],
    reviewLanguage: config.review.language,
    threshold,
    skillPromptTuning: config.pullRequestReview.skillPromptTuning || undefined,
    inlineAnchoring: true
  }
});
```

`src/commands/myPullRequestBatchReview.ts` (~512):

```ts
const review = await runHostAgentReview({
  client: input.client,
  config: input.config,
  purpose: `code-review:${input.url}`,
  input: {
    repositoryPath: input.repositoryPath,
    commitMessage: `Reviewing ${itemLabel} #${details.number}: ${details.title || "(no title)"}`,
    diff: truncatedDiff,
    changedFiles: [],
    reviewLanguage: input.config.review.language,
    threshold,
    skillPromptTuning: input.config.pullRequestReview.skillPromptTuning || undefined,
    inlineAnchoring: true
  }
});
```

Fix any other `runHostAgentReview(` compile errors in tests the same way: pass `config: createDefaultHostAgentConfig()` (or the test’s resolved config).

- [ ] **Step 5: `npm test`; commit**

Run: `npm test`

Expected: PASS.

```bash
git add src/review/hostAgentReview.ts src/commands/bridge.ts src/commands/pullRequestReview.ts src/commands/myPullRequestBatchReview.ts src/test/hostAgentReview.test.ts
git commit -m "feat: apply review skill, PR tuning override, and line locator in host-agent review"
```

---

### Task 7: Contracts, docs, examples

**Files:**
- Modify: `src/contracts.ts`, `docs/configuration.md`, `docs/parity-matrix.md`, `README.md`, `examples/config.host-agent.json`
- Test: `src/test/schemaPrint.test.ts`

- [ ] **Step 1: Extend config schema in `src/contracts.ts`**

Import `builtinSkillIds` from `./config/constants` (or `./config` if re-exported). Add:

```ts
const reviewSkillIdSchema = {
  anyOf: [{ enum: [...builtinSkillIds["code-review"]] }, { type: "string" }]
} as const;
```

In `hostAgentConfigSchema.properties.review.properties` add:

```ts
skill: {
  type: "object",
  additionalProperties: false,
  required: ["id", "path", "promptTuning"],
  properties: {
    id: reviewSkillIdSchema,
    path: stringSchema,
    promptTuning: stringSchema
  }
}
```

In `pullRequestReview.properties` add `skillPromptTuning: stringSchema`. Add `"skillPromptTuning"` to that object’s `required` array (resolved config always has the field).

Add to `src/test/schemaPrint.test.ts`:

```ts
test("schema print config-file includes review.skill and pullRequestReview.skillPromptTuning", () => {
  const result = runCli(["schema", "print", "--target", "config-file"], {});
  assert.equal(result.exitCode, EXIT_CODE_SUCCESS);
  const schema = JSON.parse(result.stdout) as Record<string, unknown>;
  const text = JSON.stringify(schema);
  assert.match(text, /"promptTuning"/);
  assert.match(text, /"skillPromptTuning"/);
  assert.match(text, /"code-review"/);
});
```

- [ ] **Step 2: Docs**

`docs/configuration.md` — after the `review.maxDiffChars` row add:

| `review.skill.id` | `code-review` | `code-review` | Change it when the repo needs domain-specific built-in review guidance | Using an unsupported id |
| `review.skill.path` | empty string | Leave empty first | Set a custom review rules file relative to the repository root | Setting a path and expecting the built-in id to still load |
| `review.skill.promptTuning` | empty string | Leave empty first | Short extra instruction when a full skill file is too heavy | Putting a handbook into one string |

Then list the 11 built-in ids (same list as CLI `docs/configuration.md`). State:

- `path` non-empty: custom file only; bundled id is ignored; classifier selected domain is `generic`
- relative `path` is resolved against the reviewed `repositoryPath`
- domain classifier can fall back to generic review when the diff does not match a specialized id
- line-number annotation and per-line finding rules apply only to `pull-request review` / `batch-review`, not staged `bridge`

In `pullRequestReview.*` table add:

| `pullRequestReview.skillPromptTuning` | empty string | Leave empty first | Override `review.skill.promptTuning` for PR/MR review only | Expecting it to change staged `bridge` review |

`docs/parity-matrix.md` — update remarks:

- `bridge --review-only` / `bridge`: 已对齐 `review.skill` + classifier；无 chunked / 无行号标注（与 CLI staged 相同）；无 repair
- `pull-request review` / `batch-review`: 已对齐 skill、classifier、`skillPromptTuning`、行号标注与 locator；无 chunked / repair

Keep the intentional-gap sentence: 无 chunked / hybrid review、无 correction repair.

`README.md` safer team example — under `review` add:

```json
"skill": {
  "id": "code-review",
  "path": "",
  "promptTuning": "Ignore findings that only complain about the project's required logging wrapper."
}
```

`examples/config.host-agent.json` — same `review.skill` block.

- [ ] **Step 3: `npm test`; commit**

Run: `npm test`

Expected: PASS.

```bash
git add src/contracts.ts src/test/schemaPrint.test.ts docs/configuration.md docs/parity-matrix.md README.md examples/config.host-agent.json
git commit -m "docs: document review.skill CLI parity and PR prompt tuning"
```

---

### Task 8: Full verification

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

Run: `npm test`

Expected: PASS.

If anything fails because a prompt snapshot assumed “no skill section”, update that assertion to require `Review skill guidance` / `Detected diff domain` instead of forbidding them. Do not weaken locator or config tests.

- [ ] **Step 2: Confirm packaged skills**

Run: `npm pack --dry-run`

Expected: output lists `src/code-review-skills/` (and still lists `src/git-commit-message-skills/`).

- [ ] **Step 3: Commit only if Step 1–2 caused extra fixes**

```bash
git add -u
git commit -m "fix: update remaining review tests for default skill prompt injection"
```

Skip this commit if the working tree is already clean.

---

## Spec coverage (self-review)

| Spec section | Task |
|--------------|------|
| §3.1 defaults | Task 1 |
| §3.2 path semantics | Task 1 (validate) + Task 2 (load) |
| §3.3 builtin ids | Task 1 |
| §3.4 parse/merge | Task 1 |
| §3.5 PR override | Task 6 |
| §4.1 call-site flags | Task 6 |
| §4.2 buildReviewMessages | Task 4 |
| §4.3 classifier | Task 3 + Task 4 |
| §4.4 locator | Task 5 + Task 6 |
| §4.5 bundled files | Task 2 |
| §5 docs/contracts | Task 7 |
| §6 tests | Tasks 1–6, 8 |
| Non-goals (no chunked/repair/flags/language/passHistory) | Global constraints; no tasks add them |
