# Plan 4 Skill Commands 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 `commit-message generate`, `pull-request create`, and `pull-request review` so host-agent covers the three skill modes at the command surface (local review already via `bridge --review-only`; 提 MR via `bridge` / `pull-request create`; 审查 MR via `pull-request review`).

**Architecture:** Reuse Plan 3 `resolveHostAgentCommitMessage`, `HostAgentClient`, session turns, and mock GitLab create. PR title/description use one `complete` turn (`purpose: "pr-content"`) returning JSON `{title,description}`. PR create uses **local** `git diff target...source` (no remote compare API). PR review uses a **fixture platform** (`--fixture-pr <json-path>`) for details+diff in Plan 4; real GitHub/GitLab HTTP is deferred to Plan 5. Zero external LLM HTTP. Zero changes to `smart-commit-cli`.

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

**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`

**Plan 4 YAGNI:**
- No hybridGenerate / correction repair turns
- No real GitHub/GitLab HTTP (mock create; fixture PR review)
- No autoApprove / autoMerge / inline or summary comment posting (actions = skipped)
- No chunked PR review
- No `titlePrompt` / `descriptionPrompt` config fields (use default prompts)
- No `report` / `my-pull-request` / `schema print` (Plan 5)

---

## File structure

| Path | Responsibility |
|------|----------------|
| `src/git.ts` | Add `getBranchDiffSnapshot(repo, target, source)` |
| `src/pullRequest/contentPrompt.ts` | PR title/description messages + responseSchema |
| `src/pullRequest/contentProtocol.ts` | Parse/validate `{title,description}` JSON |
| `src/pullRequest/hostAgentContent.ts` | Resolve provided or generate via `client.complete` |
| `src/pullRequest/urlParser.ts` | Port minimal GitHub/GitLab URL parse |
| `src/pullRequest/fixturePlatform.ts` | Load fixture PR details + diff for review |
| `src/pullRequest/mockCreate.ts` | Keep; used by `pull-request create` |
| `src/commands/commitMessageGenerate.ts` | `commit-message generate` orchestration |
| `src/commands/pullRequestCreate.ts` | `pull-request create` orchestration |
| `src/commands/pullRequestReview.ts` | `pull-request review` orchestration |
| `src/cliApp.ts` | Parse/dispatch three new commands |
| `src/test/gitBranchDiff.test.ts` | Branch diff helper |
| `src/test/prContent.test.ts` | Protocol + prompt |
| `src/test/urlParser.test.ts` | URL parser |
| `src/test/commitMessageGenerate.test.ts` | E2E generate |
| `src/test/pullRequestCreate.test.ts` | E2E create (mock) |
| `src/test/pullRequestReview.test.ts` | E2E review (fixture) |

---

### Task 1: Branch diff helper

**Files:**
- Modify: `src/git.ts`
- Test: `src/test/gitBranchDiff.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 { getBranchDiffSnapshot } from "../git";

function initRepo(): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-branch-diff-"));
  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, "README.md"), "# seed\n", "utf8");
  execFileSync("git", ["add", "README.md"], { cwd: dir });
  execFileSync("git", ["commit", "-m", "chore: seed"], { cwd: dir });
  execFileSync("git", ["branch", "-M", "main"], { cwd: dir });
  execFileSync("git", ["checkout", "-b", "feature/x"], { cwd: dir });
  fs.writeFileSync(path.join(dir, "app.ts"), "export const x = 1;\n", "utf8");
  execFileSync("git", ["add", "app.ts"], { cwd: dir });
  execFileSync("git", ["commit", "-m", "feat: add app"], { cwd: dir });
  return fs.realpathSync(dir);
}

test("getBranchDiffSnapshot returns diff and files between branches", async () => {
  const repo = initRepo();
  const snap = await getBranchDiffSnapshot(repo, "main", "feature/x");
  assert.match(snap.diff, /app\.ts/);
  assert.deepEqual(snap.changedFiles, ["app.ts"]);
  assert.ok(snap.fullDiffChars > 0);
  assert.match(snap.commitsText, /feat: add app/);
});
```

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

Run: `npm test -- --test-name-pattern='getBranchDiffSnapshot'`  
Expected: FAIL (export missing) or compile error.

- [ ] **Step 3: Implement in `src/git.ts`**

```typescript
export interface BranchDiffSnapshot {
  diff: string;
  changedFiles: string[];
  fullDiffChars: number;
  commitsText: string;
}

export async function getBranchDiffSnapshot(
  repositoryPath: string,
  targetBranch: string,
  sourceBranch: string
): Promise<BranchDiffSnapshot> {
  const range = `${targetBranch}...${sourceBranch}`;
  const { stdout: diff } = await runGit(
    repositoryPath,
    ["diff", "--no-ext-diff", "--unified=3", range],
    `git diff ${range}`
  );
  const { stdout: names } = await runGit(
    repositoryPath,
    ["diff", "--name-only", "--diff-filter=ACMR", range],
    `git diff --name-only ${range}`
  );
  const { stdout: commits } = await runGit(
    repositoryPath,
    ["log", "--pretty=format:%s", range],
    `git log ${range}`
  );
  return {
    diff,
    changedFiles: names
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter((line) => line.length > 0),
    fullDiffChars: diff.length,
    commitsText: commits.trim()
  };
}
```

- [ ] **Step 4: `npm test` green; commit** `feat: add getBranchDiffSnapshot git helper`

---

### Task 2: PR content prompt + protocol + host-agent resolve

**Files:**
- Create: `src/pullRequest/contentPrompt.ts`, `contentProtocol.ts`, `hostAgentContent.ts`
- Test: `src/test/prContent.test.ts`

- [ ] **Step 1: Protocol tests**

```typescript
import assert from "node:assert/strict";
import test from "node:test";
import {
  PullRequestContentProtocolError,
  parsePullRequestContentResponse
} from "../pullRequest/contentProtocol";
import { buildPullRequestContentMessages, PR_CONTENT_RESPONSE_SCHEMA } from "../pullRequest/contentPrompt";

test("parsePullRequestContentResponse accepts title and description", () => {
  const parsed = parsePullRequestContentResponse(
    JSON.stringify({ title: "Add login", description: "## Summary\n- login" })
  );
  assert.equal(parsed.title, "Add login");
  assert.equal(parsed.description, "## Summary\n- login");
});

test("parsePullRequestContentResponse rejects empty title", () => {
  assert.throws(
    () => parsePullRequestContentResponse(JSON.stringify({ title: "  ", description: "ok" })),
    (e: unknown) => e instanceof PullRequestContentProtocolError
  );
});

test("buildPullRequestContentMessages includes branches and schema", () => {
  const messages = buildPullRequestContentMessages({
    repositoryName: "demo",
    platform: "gitlab",
    sourceBranch: "feature/x",
    targetBranch: "main",
    changedFiles: ["a.ts"],
    diff: "diff --git a/a.ts",
    fullDiffChars: 20,
    truncated: false,
    commitsText: "feat: a",
    language: "zh-cn"
  });
  assert.equal(messages.length, 2);
  assert.match(messages[1]!.content, /feature\/x/);
  assert.match(PR_CONTENT_RESPONSE_SCHEMA, /title/);
});
```

- [ ] **Step 2: Implement protocol** (port strip-fence + JSON validate from CLI `contentService.ts`; no correction retries).

- [ ] **Step 3: Implement prompt** (simplify CLI `contentPrompt.ts`; no titlePrompt/descriptionPrompt tuning).

- [ ] **Step 4: Implement `resolveHostAgentPullRequestContent`**

```typescript
export async function resolveHostAgentPullRequestContent(input: {
  client: HostAgentClient;
  repositoryName: string;
  platform: "github" | "gitlab";
  sourceBranch: string;
  targetBranch: string;
  changedFiles: string[];
  diff: string;
  fullDiffChars: number;
  truncated: boolean;
  commitsText: string;
  language: string;
  providedTitle?: string;
  providedDescription?: string;
}): Promise<{ title: string; description: string; source: "provided" | "generated" }> {
  const title = (input.providedTitle ?? "").trim();
  const description = (input.providedDescription ?? "").trim();
  if (title && description) {
    return { title, description, source: "provided" };
  }
  if (title !== description && (title || description)) {
    throw new Error("Both --title and --description are required when providing PR content.");
  }
  const raw = await input.client.complete(
    buildPullRequestContentMessages({ ... }),
    { purpose: "pr-content", responseSchema: PR_CONTENT_RESPONSE_SCHEMA, attempt: 0 }
  );
  const parsed = parsePullRequestContentResponse(raw);
  return { ...parsed, source: "generated" };
}
```

- [ ] **Step 5: Tests green; commit** `feat: add host-agent PR content resolve`

---

### Task 3: `commit-message generate` command

**Files:**
- Create: `src/commands/commitMessageGenerate.ts`
- Modify: `src/cliApp.ts`
- Test: `src/test/commitMessageGenerate.test.ts`
- Modify: `src/test/cliApp.test.ts` (help lists command)

**Flow:**
1. Resolve config (`requireConnection: false`); `--repo` required
2. Preflight staged diff (same auto-stage rules as bridge)
3. Open/reuse session; `resolveHostAgentCommitMessage`
4. Catch `NeedsHostAgentError` → status `needs_host_agent`, exit 10
5. Success → status `provided` | `generated`, exit 0
6. Blocked (no changes / no staged) → exit 2

**Flags:** `--repo`, `--session`, `--session-base`, `--commit-message`, `--config`, `--output`

- [ ] **Step 1: Failing E2E** — provided message returns `status: "provided"` without session turn; autoGenerate path: needs → write response → `generated`.

- [ ] **Step 2: Implement command + wire `parseCliCommand` / `runCliAsync`** for `commit-message generate`.

- [ ] **Step 3: `npm test` green; commit** `feat: add commit-message generate command`

---

### Task 4: URL parser + fixture PR platform

**Files:**
- Create: `src/pullRequest/urlParser.ts`, `fixturePlatform.ts`
- Test: `src/test/urlParser.test.ts`, extend fixture load in review tests

- [ ] **Step 1: URL parser tests** for GitHub `.../pull/12` and GitLab `.../-/merge_requests/34`; empty/invalid throw.

- [ ] **Step 2: Port `parsePullRequestUrl`** from CLI (minimal; hardcode platform html/api base for github.com / gitlab.com; private host requires `pullRequest.provider`).

- [ ] **Step 3: Fixture shape + loader**

```typescript
export interface FixturePullRequest {
  platform: "github" | "gitlab";
  number: number;
  title: string;
  webUrl: string;
  isDraft: boolean;
  isOpen: boolean;
  isMerged: boolean;
  state: string;
  diff: string;
}

export function loadFixturePullRequest(filePath: string): FixturePullRequest
```

Validate required fields; reject merged/closed at command layer.

- [ ] **Step 4: Commit** `feat: add PR URL parser and fixture platform`

---

### Task 5: `pull-request create` command

**Files:**
- Create: `src/commands/pullRequestCreate.ts`
- Modify: `src/cliApp.ts`
- Test: `src/test/pullRequestCreate.test.ts`

**Flow:**
1. Config + `--repo`; optional `--dry-run`, `--title`, `--description`, `--session`
2. `getCurrentBranch`; reject HEAD / skipBranches / source===target
3. Platform fixed to `gitlab` for mock path (or detect from config.provider when not auto; Plan 4 default mock gitlab)
4. `getBranchDiffSnapshot(target, source)`; empty diff → RUNTIME_ERROR
5. Truncate to `pullRequestCreation.maxDiffChars`
6. `resolveHostAgentPullRequestContent` (turn or provided)
7. If dry-run → `status: "ready"`, url null
8. Else `createMockGitLabPullRequestCreation` → `status: "created"`
9. needs_host_agent on pause

- [ ] **Step 1: E2E provided title+description + dry-run → ready**
- [ ] **Step 2: E2E auto content → needs → fixture JSON → created (mock URL)**
- [ ] **Step 3: skipBranches / same-branch → CONFIG or RUNTIME error**
- [ ] **Step 4: Wire CLI; tests green; commit** `feat: add pull-request create with mock GitLab`

---

### Task 6: `pull-request review` command

**Files:**
- Create: `src/commands/pullRequestReview.ts`
- Modify: `src/cliApp.ts`
- Test: `src/test/pullRequestReview.test.ts`

**Flow:**
1. Positional URL after `pull-request review`; `--repo`, `--fixture-pr` **required in Plan 4** (clear error if missing: real API deferred)
2. Parse URL; load fixture; assert fixture.number matches URL number and platform kind matches
3. Reject merged / not open
4. Session + `runHostAgentReview` on fixture.diff (threshold from `pullRequestReview.threshold`)
5. needs_host_agent / passed / blocked
6. Output fields aligned with CLI subset: `url`, `platform`, `number`, `title`, `webUrl`, `score`, `threshold`, `passed`, `details`, `summaryCommentAction: "skipped"`, `inlineCommentAction: "skipped"`, approve/merge attempted false

- [ ] **Step 1: E2E needs → pass fixture response → passed**
- [ ] **Step 2: E2E block score → blocked exit 2**
- [ ] **Step 3: Missing `--fixture-pr` → CONFIG_ERROR mentioning Plan 5**
- [ ] **Step 4: Wire CLI; commit** `feat: add pull-request review with fixture platform`

---

### Task 7: Docs + roadmap

**Files:**
- Update: `docs/parity-matrix.md`, `docs/superpowers/plans/2026-08-10-host-agent-roadmap.md`, `README.md`

- [ ] Mark Plan 4 commands as partial (mock/fixture; no real API)
- [ ] Roadmap: Plan 4 complete; next Plan 5
- [ ] README: document the three commands + `--fixture-pr`
- [ ] Commit `docs: mark Plan 4 skill commands complete`

---

## Acceptance

- `npm test` all green
- `commit-message generate` fixture path: needs → generated
- `pull-request create`: provided dry-run ready; generated → mock created
- `pull-request review`: fixture + turn → passed/blocked; `--fixture-pr` required
- `--review-only` / full `bridge` still work
- Zero LLM HTTP; zero changes to `smart-commit-cli`

## Spec coverage (self-review)

| Spec §4 command | Task |
|-----------------|------|
| `commit-message generate` | Task 3 |
| `pull-request create` | Tasks 2, 5 |
| `pull-request review` | Tasks 4, 6 |
| Skill three modes | review-only (Plan 2) + create (Task 5) + review (Task 6) |

**Deferred to Plan 5:** real platform HTTP, skill `bugfix-gitlab-mr-qax` switch, `report` / `my-pull-request` / `schema print`.
