# PR/MR Creation 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 `pullRequestCreation` (configFilePath, titlePrompt, descriptionPrompt, reviewers, milestone, draft), `pullRequestReview.configFilePath` overlay loading, pr-content prompt tuning, and GitHub/GitLab create API with `smart-commit-cli` 0.1.21; tighten config root key to `smartCommitHostAgent` only.

**Architecture:** Surgical port into existing `src/config/*`, `src/pullRequest/contentPrompt.ts`, `src/pullRequest/hostAgentContent.ts`, `src/pullRequest/createApi.ts`, and create/bridge commands. New `src/config/overlay.ts` shared by creation and review overlays. Two-phase resolve: main file → overlay → env refs. Do not replace whole files or extract a shared npm package.

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

**Spec:** `docs/superpowers/specs/2026-08-15-pr-creation-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 PR content repair turns
- Do not add `--pull-request-creation-*` / `SMART_COMMIT_PULL_REQUEST_CREATION_*`
- Do not change `autoCreateAfterPush` / `removeSourceBranch` host-agent defaults (`true`)
- Do not accept `smartCommitCli` as a config or overlay root key
- Do not write a migration guide
- Canonical object **internal** unknown keys stay silently ignored

---

## File structure

| Path | Responsibility |
|------|----------------|
| `src/config/schema.ts` | Add creation + review overlay fields to `HostAgentConfig` |
| `src/config/defaults.ts` | New-field defaults matching CLI |
| `src/config/load.ts` | Parse new fields; merge `reviewers`; require `smartCommitHostAgent` only; call overlay after main file |
| `src/config/overlay.ts` | Shared overlay loader (path list, first existing file, section + forbidden keys) |
| `src/config/resolve.ts` | Parse `--repo` for overlay `baseDirectory`; pass it into load |
| `src/pullRequest/contentPrompt.ts` | Inject Title/Description prompt tuning sections |
| `src/pullRequest/hostAgentContent.ts` | Pass prompts; `resolvePullRequestTitle(..., titlePrompt)` |
| `src/pullRequest/createApi.ts` | GitHub reviewers/milestone warnings; GitLab reviewer_ids/milestone_id; honor `draft` |
| `src/commands/bridge.ts` | Pass prompts + `reviewers`/`milestone`/`draft` from config |
| `src/commands/pullRequestCreate.ts` | Same |
| `src/contracts.ts` | Config schema for new fields |
| Tests | `configResolve`, `prContent`, `createApi`, `pullRequestCreate` |
| Docs | configuration, parity-matrix, README, example, CHANGELOG |

---

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

**Files:**
- Modify: `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("pullRequestCreation new fields default to CLI empty values", () => {
  const resolved = resolveHostAgentConfig({ env: {} });
  assert.equal(resolved.config.pullRequestCreation.configFilePath, "");
  assert.equal(resolved.config.pullRequestCreation.titlePrompt, "");
  assert.equal(resolved.config.pullRequestCreation.descriptionPrompt, "");
  assert.deepEqual(resolved.config.pullRequestCreation.reviewers, []);
  assert.equal(resolved.config.pullRequestCreation.milestone, "");
  assert.equal(resolved.config.pullRequestCreation.draft, false);
  assert.equal(resolved.config.pullRequestReview.configFilePath, "");
  assert.equal(resolved.config.pullRequestCreation.autoCreateAfterPush, true);
  assert.equal(resolved.config.pullRequestCreation.removeSourceBranch, true);
});

test("pullRequestCreation string fields trim and reviewers replace on merge", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        pullRequestCreation: {
          configFilePath: "  .pr.creation.json  ",
          titlePrompt: "  Keep it short  ",
          descriptionPrompt: "  Mention risks.  ",
          reviewers: [" alice ", "", "bob"],
          milestone: "  42  ",
          draft: true
        }
      }
    }),
    "utf8"
  );
  const resolved = resolveHostAgentConfig({ configPath, env: {} });
  assert.equal(resolved.config.pullRequestCreation.configFilePath, ".pr.creation.json");
  assert.equal(resolved.config.pullRequestCreation.titlePrompt, "Keep it short");
  assert.equal(resolved.config.pullRequestCreation.descriptionPrompt, "Mention risks.");
  assert.deepEqual(resolved.config.pullRequestCreation.reviewers, ["alice", "bob"]);
  assert.equal(resolved.config.pullRequestCreation.milestone, "42");
  assert.equal(resolved.config.pullRequestCreation.draft, true);
});
```

Leave `configFilePath` pointing at a **non-existent** file only after Task 3 (overlay load). In this task the field is stored but overlay is not loaded yet, so a missing file must **not** throw. Use a path that does not need to exist.

- [ ] **Step 2: Run tests to verify they fail**

Run: `npm test -- --test-name-pattern="pullRequestCreation new fields"`

Expected: FAIL (property undefined / TypeScript compile error on new fields).

- [ ] **Step 3: Minimal implementation**

`src/config/schema.ts` — extend `HostAgentConfig`:

```ts
  pullRequestCreation: {
    autoCreateAfterPush: boolean;
    configFilePath: string;
    targetBranch: string;
    titlePrompt: string;
    descriptionPrompt: string;
    maxDiffChars: number;
    assignees: string[];
    reviewers: string[];
    labels: string[];
    milestone: string;
    draft: boolean;
    removeSourceBranch: boolean;
    skipBranches: string[];
  };
  pullRequestReview: {
    threshold: number;
    autoApprove: boolean;
    autoMerge: boolean;
    summarySeverities: PullRequestReviewSeverity[];
    commentSeverities: PullRequestReviewSeverity[];
    skillPromptTuning: string;
    skipSummaryOnPass: boolean;
    skipCommentOnPass: boolean;
    configFilePath: string;
  };
```

`src/config/defaults.ts`:

```ts
    pullRequestCreation: {
      autoCreateAfterPush: true,
      configFilePath: "",
      targetBranch: "",
      titlePrompt: "",
      descriptionPrompt: "",
      maxDiffChars: 200_000,
      assignees: [],
      reviewers: [],
      labels: [],
      milestone: "",
      draft: false,
      removeSourceBranch: true,
      skipBranches: ["main", "master", "develop"]
    },
    pullRequestReview: {
      // existing fields unchanged
      configFilePath: ""
    },
```

`src/config/load.ts` — in `parseCanonicalHostAgentConfig` `pullRequestCreation` object, add:

```ts
      ...(Object.hasOwn(pullRequestCreation, "configFilePath")
        ? {
            configFilePath: parseString(
              pullRequestCreation.configFilePath,
              `${sourceLabel}.pullRequestCreation.configFilePath`
            ).trim()
          }
        : {}),
      ...(Object.hasOwn(pullRequestCreation, "titlePrompt")
        ? {
            titlePrompt: parseString(
              pullRequestCreation.titlePrompt,
              `${sourceLabel}.pullRequestCreation.titlePrompt`
            ).trim()
          }
        : {}),
      ...(Object.hasOwn(pullRequestCreation, "descriptionPrompt")
        ? {
            descriptionPrompt: parseString(
              pullRequestCreation.descriptionPrompt,
              `${sourceLabel}.pullRequestCreation.descriptionPrompt`
            ).trim()
          }
        : {}),
      ...(Object.hasOwn(pullRequestCreation, "reviewers")
        ? {
            reviewers: parseStringArray(
              pullRequestCreation.reviewers,
              `${sourceLabel}.pullRequestCreation.reviewers`
            )
          }
        : {}),
      ...(Object.hasOwn(pullRequestCreation, "milestone")
        ? {
            milestone: parseString(
              pullRequestCreation.milestone,
              `${sourceLabel}.pullRequestCreation.milestone`
            ).trim()
          }
        : {}),
      ...(Object.hasOwn(pullRequestCreation, "draft")
        ? {
            draft: parseBoolean(
              pullRequestCreation.draft,
              `${sourceLabel}.pullRequestCreation.draft`
            )
          }
        : {}),
```

In the `pullRequestReview` parse object, add:

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

In `mergeHostAgentConfig`:

```ts
    pullRequestCreation: {
      ...base.pullRequestCreation,
      ...override.pullRequestCreation,
      assignees: override.pullRequestCreation?.assignees
        ? [...override.pullRequestCreation.assignees]
        : [...base.pullRequestCreation.assignees],
      reviewers: override.pullRequestCreation?.reviewers
        ? [...override.pullRequestCreation.reviewers]
        : [...base.pullRequestCreation.reviewers],
      labels: override.pullRequestCreation?.labels
        ? [...override.pullRequestCreation.labels]
        : [...base.pullRequestCreation.labels],
      skipBranches: override.pullRequestCreation?.skipBranches
        ? [...override.pullRequestCreation.skipBranches]
        : [...base.pullRequestCreation.skipBranches]
    },
```

Fix every in-test / fixture `HostAgentConfig` literal that constructs `pullRequestCreation` by hand (e.g. `src/test/passHistory.test.ts` if present) so TypeScript compiles: add the six new fields with empty defaults.

- [ ] **Step 4: Run tests to verify they pass**

Run: `npm test -- --test-name-pattern="pullRequestCreation new fields|string fields trim"`

Expected: PASS. Then `npm test` — fix any `HostAgentConfig` literals that fail `tsc`.

- [ ] **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 "feat: add pullRequestCreation overlay and prompt fields to config"
```

Include any fixture files you had to update for `tsc`.

---

### Task 2: Root key only `smartCommitHostAgent`

**Files:**
- Modify: `src/config/load.ts` (`extractConfigFromRoot`)
- Test: `src/test/configResolve.test.ts`

- [ ] **Step 1: Replace the two migration tests** in `src/test/configResolve.test.ts`

Delete or rewrite:

- `resolveHostAgentConfig loads smartCommitCli and strips connection`
- `resolveHostAgentConfig prefers smartCommitHostAgent over smartCommitCli`

Add:

```ts
test("resolveHostAgentConfig rejects smartCommitCli root key", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitCli: {
        review: { threshold: 8 }
      }
    }),
    "utf8"
  );
  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {} }),
    /smartCommitHostAgent/
  );
});

test("resolveHostAgentConfig rejects both smartCommitHostAgent and smartCommitCli", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: { review: { threshold: 3 } },
      smartCommitCli: { review: { threshold: 9 } }
    }),
    "utf8"
  );
  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {} }),
    /unsupported key: smartCommitCli/
  );
});

test("resolveHostAgentConfig still strips connection inside smartCommitHostAgent", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        review: { threshold: 8, language: "en" },
        connection: {
          apiKey: "sk-test",
          baseUrl: "https://api.example.com",
          model: "gpt-4"
        }
      }
    }),
    "utf8"
  );
  const resolved = resolveHostAgentConfig({ configPath, env: {} });
  assert.equal(resolved.config.review.threshold, 8);
  assert.equal("connection" in resolved.config, false);
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `npm test -- --test-name-pattern="rejects smartCommitCli|rejects both smartCommitHostAgent"`

Expected: FAIL — current loader still accepts `smartCommitCli`.

- [ ] **Step 3: Implement root-key check** in `extractConfigFromRoot` (`src/config/load.ts`)

```ts
function rejectUnsupportedTopLevelKeys(root: Record<string, unknown>, sourceLabel: string): void {
  const unsupportedKey = Object.keys(root).find((key) => key !== "smartCommitHostAgent");
  if (!unsupportedKey) {
    return;
  }
  throw new Error(
    `${sourceLabel} only supports the smartCommitHostAgent config object; unsupported key: ${unsupportedKey}.`
  );
}

function extractConfigFromRoot(parsed: unknown, configPath: string): DeepPartial<HostAgentConfig> {
  const root = parseObject(parsed, `Host-agent config file ${configPath}`);
  rejectUnsupportedTopLevelKeys(root, `Host-agent config file ${configPath}`);
  if (!Object.hasOwn(root, "smartCommitHostAgent")) {
    throw new Error(`Host-agent config file must contain smartCommitHostAgent: ${configPath}`);
  }
  return parseCanonicalHostAgentConfig(root.smartCommitHostAgent, `${configPath}:smartCommitHostAgent`);
}
```

Export `rejectUnsupportedTopLevelKeys` from `load.ts` **or** put it in `overlay.ts` in Task 3 and import it here. Prefer putting the function in `src/config/overlay.ts` in this task if you create that file now; otherwise keep it local and move in Task 3. Do not duplicate two different error strings.

`parseCanonicalHostAgentConfig` already strips `connection` inside the canonical object — keep that.

- [ ] **Step 4: Run tests**

Run: `npm test -- --test-name-pattern="rejects smartCommitCli|rejects both|strips connection inside"`

Expected: PASS. Then `npm test`.

- [ ] **Step 5: Commit**

```bash
git add src/config/load.ts src/test/configResolve.test.ts
git commit -m "feat: accept only smartCommitHostAgent as config root key"
```

---

### Task 3: Overlay loader

**Files:**
- Create: `src/config/overlay.ts`
- Modify: `src/config/load.ts`, `src/config/resolve.ts`
- Test: `src/test/configResolve.test.ts`

- [ ] **Step 1: Write failing overlay tests** in `src/test/configResolve.test.ts`

```ts
function writeJson(filePath: string, value: unknown): void {
  fs.writeFileSync(filePath, JSON.stringify(value), "utf8");
}

test("pullRequestCreation.configFilePath overlay wins over main config", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const overlayPath = path.join(dir, ".smart-commit-pr.creation.json");
  writeJson(overlayPath, {
    smartCommitHostAgent: {
      pullRequestCreation: {
        titlePrompt: "overlay title",
        reviewers: ["overlay-reviewer"]
      }
    }
  });
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestCreation: {
        configFilePath: ".smart-commit-pr.creation.json",
        titlePrompt: "main title",
        reviewers: ["main-reviewer"]
      }
    }
  });
  const resolved = resolveHostAgentConfig({
    configPath,
    env: {},
    argv: ["--repo", dir]
  });
  assert.equal(resolved.config.pullRequestCreation.titlePrompt, "overlay title");
  assert.deepEqual(resolved.config.pullRequestCreation.reviewers, ["overlay-reviewer"]);
  assert.equal(resolved.config.pullRequestCreation.configFilePath, ".smart-commit-pr.creation.json");
});

test("pullRequestCreation.configFilePath picks the first existing comma-separated file", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  writeJson(path.join(dir, ".smart-commit-pr.creation.json"), {
    smartCommitHostAgent: {
      pullRequestCreation: { milestone: "from-second" }
    }
  });
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestCreation: {
        configFilePath: ".missing.json, .smart-commit-pr.creation.json"
      }
    }
  });
  const resolved = resolveHostAgentConfig({ configPath, env: {}, argv: ["--repo", dir] });
  assert.equal(resolved.config.pullRequestCreation.milestone, "from-second");
});

test("empty pullRequestCreation.configFilePath skips overlay", () => {
  const resolved = resolveHostAgentConfig({ env: {} });
  assert.equal(resolved.config.pullRequestCreation.titlePrompt, "");
});

test("missing overlay files list tried paths", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestCreation: { configFilePath: ".nope.json" }
    }
  });
  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {}, argv: ["--repo", dir] }),
    /pullRequestCreation\.configFilePath not found[\s\S]*\.nope\.json/
  );
});

test("creation overlay rejects nested configFilePath and autoCreateAfterPush", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  writeJson(path.join(dir, ".overlay.json"), {
    smartCommitHostAgent: {
      pullRequestCreation: { configFilePath: ".nested.json" }
    }
  });
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestCreation: { configFilePath: ".overlay.json" }
    }
  });
  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {}, argv: ["--repo", dir] }),
    /configFilePath is not supported inside a pull request creation config file/
  );
});

test("creation overlay rejects smartCommitCli root", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  writeJson(path.join(dir, ".overlay.json"), {
    smartCommitCli: {
      pullRequestCreation: { titlePrompt: "cli overlay" }
    }
  });
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestCreation: { configFilePath: ".overlay.json" }
    }
  });
  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {}, argv: ["--repo", dir] }),
    /unsupported key: smartCommitCli/
  );
});

test("empty overlay object applies no overrides", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  writeJson(path.join(dir, ".overlay.json"), {});
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestCreation: {
        configFilePath: ".overlay.json",
        titlePrompt: "main title"
      }
    }
  });
  const resolved = resolveHostAgentConfig({ configPath, env: {}, argv: ["--repo", dir] });
  assert.equal(resolved.config.pullRequestCreation.titlePrompt, "main title");
});

test("pullRequestReview.configFilePath overlay wins over main config", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  writeJson(path.join(dir, ".smart-commit-pr.review.json"), {
    smartCommitHostAgent: {
      pullRequestReview: { skillPromptTuning: "overlay tuning" }
    }
  });
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestReview: {
        configFilePath: ".smart-commit-pr.review.json",
        skillPromptTuning: "main tuning"
      }
    }
  });
  const resolved = resolveHostAgentConfig({ configPath, env: {}, argv: ["--repo", dir] });
  assert.equal(resolved.config.pullRequestReview.skillPromptTuning, "overlay tuning");
  assert.equal(resolved.config.pullRequestReview.configFilePath, ".smart-commit-pr.review.json");
});

test("review overlay rejects nested configFilePath", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  writeJson(path.join(dir, ".overlay.json"), {
    smartCommitHostAgent: {
      pullRequestReview: { configFilePath: ".nested.json" }
    }
  });
  const configPath = path.join(dir, "cfg.json");
  writeJson(configPath, {
    smartCommitHostAgent: {
      pullRequestReview: { configFilePath: ".overlay.json" }
    }
  });
  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {}, argv: ["--repo", dir] }),
    /configFilePath is not supported inside a pull request review config file/
  );
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `npm test -- --test-name-pattern="overlay wins|comma-separated|missing overlay|rejects nested|rejects smartCommitCli root|empty overlay object|pullRequestReview.configFilePath overlay"`

Expected: FAIL — overlay not loaded; missing file currently stored as a string with no throw.

- [ ] **Step 3: Implement `src/config/overlay.ts`**

Port path-list helpers from CLI `src/config/file.ts` (`parseConfigFilePathList`, `resolveFirstExistingConfigFilePath`, `readJsonConfigFile`). Overlay root uses the same top-level rule as Task 2 (`smartCommitHostAgent` only). Empty `{}` returns `undefined`.

```ts
import fs from "node:fs";
import path from "node:path";
import { DeepPartial, HostAgentConfig, parseObject } from "./schema";
import { parseCanonicalHostAgentConfig } from "./load";

export function rejectUnsupportedTopLevelKeys(root: Record<string, unknown>, sourceLabel: string): void {
  const unsupportedKey = Object.keys(root).find((key) => key !== "smartCommitHostAgent");
  if (!unsupportedKey) {
    return;
  }
  throw new Error(
    `${sourceLabel} only supports the smartCommitHostAgent config object; unsupported key: ${unsupportedKey}.`
  );
}

export function loadPullRequestCreationConfigFile(
  configuredValue: string,
  baseDirectory: string
): DeepPartial<HostAgentConfig> | undefined {
  return loadSectionOverlayFile(
    configuredValue,
    baseDirectory,
    "pullRequestCreation.configFilePath",
    "pull request creation config file",
    "pullRequestCreation",
    ["autoCreateAfterPush", "configFilePath"]
  );
}

export function loadPullRequestReviewConfigFile(
  configuredValue: string,
  baseDirectory: string
): DeepPartial<HostAgentConfig> | undefined {
  return loadSectionOverlayFile(
    configuredValue,
    baseDirectory,
    "pullRequestReview.configFilePath",
    "pull request review config file",
    "pullRequestReview",
    ["configFilePath"]
  );
}

function loadSectionOverlayFile(
  configuredValue: string,
  baseDirectory: string,
  fieldName: string,
  fileLabel: string,
  section: "pullRequestCreation" | "pullRequestReview",
  forbiddenKeys: string[]
): DeepPartial<HostAgentConfig> | undefined {
  const configuredPaths = parseConfigFilePathList(configuredValue);
  if (configuredPaths.length === 0) {
    return undefined;
  }
  const resolvedPath = resolveFirstExistingConfigFilePath(configuredPaths, baseDirectory, fieldName);
  const content = readJsonConfigFile(resolvedPath, fileLabel);
  const root = parseObject(content, `${fileLabel} ${resolvedPath}`);
  rejectUnsupportedTopLevelKeys(root, `${fileLabel} ${resolvedPath}`);
  if (!Object.hasOwn(root, "smartCommitHostAgent")) {
    return undefined;
  }
  const canonical = parseObject(root.smartCommitHostAgent, `${resolvedPath}:smartCommitHostAgent`);
  const extraKey = Object.keys(canonical).find((key) => key !== section);
  if (extraKey) {
    throw new Error(
      `${resolvedPath}:smartCommitHostAgent only supports ${section} settings; unsupported key: ${extraKey}.`
    );
  }
  if (!Object.hasOwn(canonical, section)) {
    return {};
  }
  const sectionObject = parseObject(canonical[section], `${resolvedPath}:smartCommitHostAgent.${section}`);
  for (const forbiddenKey of forbiddenKeys) {
    if (Object.hasOwn(sectionObject, forbiddenKey)) {
      throw new Error(
        `${resolvedPath}:smartCommitHostAgent.${section}.${forbiddenKey} is not supported inside a ${fileLabel}.`
      );
    }
  }
  return parseCanonicalHostAgentConfig({ [section]: sectionObject }, `${resolvedPath}:smartCommitHostAgent`);
}

function parseConfigFilePathList(configuredValue: string): string[] {
  return configuredValue
    .split(",")
    .map((part) => part.trim())
    .filter((part) => part.length > 0);
}

function resolveFirstExistingConfigFilePath(
  configuredPaths: string[],
  baseDirectory: string,
  fieldName: string
): string {
  const attemptedPaths: Array<{ configuredPath: string; resolvedPath: string }> = [];
  for (const configuredPath of configuredPaths) {
    const resolvedPath = path.isAbsolute(configuredPath)
      ? path.normalize(configuredPath)
      : path.resolve(baseDirectory, configuredPath);
    attemptedPaths.push({ configuredPath, resolvedPath });
    if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) {
      return resolvedPath;
    }
  }
  const triedPaths = attemptedPaths
    .map(({ configuredPath, resolvedPath }) => `${configuredPath} (resolved to ${resolvedPath})`)
    .join("; ");
  throw new Error(`Configured ${fieldName} not found. Tried: ${triedPaths}`);
}

function readJsonConfigFile(filePath: string, label: string): unknown {
  let content: string;
  try {
    content = fs.readFileSync(filePath, "utf8");
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`Failed to read ${label}: ${filePath}. ${message}`);
  }
  if (!content.trim()) {
    throw new Error(`${label} must not be empty: ${filePath}`);
  }
  try {
    return JSON.parse(content);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`${label} is not valid JSON: ${filePath}. ${message}`);
  }
}
```

If `parseCanonicalHostAgentConfig` / `rejectUnsupportedTopLevelKeys` would create a circular import (`load.ts` ↔ `overlay.ts`), keep JSON/path helpers in `overlay.ts` and pass a parse callback, **or** move `parseCanonicalHostAgentConfig` export usage so overlay only returns the raw section object and `load.ts` merges it. Do not introduce a cycle. Preferred: `overlay.ts` does not import `load.ts`; it duplicates the small forbidden-key checks and returns `DeepPartial<HostAgentConfig>` by calling a parse function injected from `load.ts`:

```ts
export function loadPullRequestCreationConfigFile(
  configuredValue: string,
  baseDirectory: string,
  parseCanonical: (raw: unknown, sourceLabel: string) => DeepPartial<HostAgentConfig>
): DeepPartial<HostAgentConfig> | undefined
```

Call site in `load.ts` passes `parseCanonicalHostAgentConfig`.

Switch Task 2's `extractConfigFromRoot` to import `rejectUnsupportedTopLevelKeys` from `overlay.ts` so there is one implementation.

- [ ] **Step 4: Wire two-phase load**

`src/config/resolve.ts` — parse `--repo` the same way `--config` is parsed (support `--repo <path>` and `--repo=`):

```ts
export function resolveHostAgentConfig(input: {
  configPath?: string;
  env?: NodeJS.ProcessEnv;
  argv?: string[];
}): ResolvedHostAgentConfig {
  const env = input.env ?? process.env;
  const argv = input.argv ?? [];
  rejectLlmFlags(argv);

  let configPath = input.configPath;
  if (!configPath) {
    configPath = parseConfigPathFromArgv(argv);
  }
  const baseDirectory = parseRepoPathFromArgv(argv) ?? process.cwd();
  const config = configPath
    ? loadHostAgentConfig(configPath, env, baseDirectory)
    : resolveDefaultHostAgentConfig(env);

  return { config, configPath: configPath ?? null };
}
```

`src/config/load.ts` — change `loadHostAgentConfig` to:

1. Read + parse main file (no overlay).
2. `mergeHostAgentConfig(defaults, fileConfig)`.
3. `resolveEnvReferences` (so `env:` in `configFilePath` works).
4. Load creation overlay then review overlay (later overlay wins on overlapping keys; they should not overlap).
5. Merge overlays on top.
6. `resolveEnvReferences` again.
7. `validateHostAgentConfig`.

```ts
export function loadHostAgentConfig(
  configPath: string,
  env: NodeJS.ProcessEnv,
  baseDirectory: string = process.cwd()
): HostAgentConfig {
  const fileConfig = readAndParseConfigFile(configPath);
  const preliminary = resolveEnvReferences(
    mergeHostAgentConfig(createDefaultHostAgentConfig(), fileConfig),
    env
  );
  const creationOverlay = loadPullRequestCreationConfigFile(
    preliminary.pullRequestCreation.configFilePath,
    baseDirectory,
    parseCanonicalHostAgentConfig
  );
  const reviewOverlay = loadPullRequestReviewConfigFile(
    preliminary.pullRequestReview.configFilePath,
    baseDirectory,
    parseCanonicalHostAgentConfig
  );
  const merged = mergeHostAgentConfig(
    mergeHostAgentConfig(preliminary, creationOverlay ?? {}),
    reviewOverlay ?? {}
  );
  const resolved = resolveEnvReferences(merged, env);
  validateHostAgentConfig(resolved);
  return resolved;
}
```

Split today's `loadHostAgentConfig` body so `readAndParseConfigFile` only does fs + JSON + `extractConfigFromRoot`.

- [ ] **Step 5: Run tests**

Run: `npm test -- --test-name-pattern="overlay|configFilePath|smartCommitCli"`

Expected: PASS. Then `npm test`.

- [ ] **Step 6: Commit**

```bash
git add src/config/overlay.ts src/config/load.ts src/config/resolve.ts src/test/configResolve.test.ts
git commit -m "feat: load pullRequestCreation and review overlay config files"
```

---

### Task 4: Prompt tuning and title resolution

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

- [ ] **Step 1: Write failing tests** in `src/test/prContent.test.ts`

Update existing `resolvePullRequestTitle` tests to pass `""` as the third argument (new required parameter). Add:

```ts
test("buildPullRequestContentMessages injects title and description prompt tuning", () => {
  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",
    titlePrompt: "Keep the title short.",
    descriptionPrompt: "Mention risks only."
  });
  assert.match(messages[1]!.content, /Title prompt tuning:\nKeep the title short\./);
  assert.match(messages[1]!.content, /Description prompt tuning:\nMention risks only\./);
  const tuningIndex = messages[1]!.content.indexOf("Title prompt tuning:");
  const filesIndex = messages[1]!.content.indexOf("Changed files");
  assert.ok(tuningIndex >= 0 && filesIndex > tuningIndex);
});

test("buildPullRequestContentMessages omits empty prompt tuning", () => {
  const messages = buildPullRequestContentMessages({
    repositoryName: "demo",
    platform: "gitlab",
    sourceBranch: "feature/x",
    targetBranch: "main",
    changedFiles: ["a.ts"],
    diff: "diff",
    fullDiffChars: 4,
    truncated: false,
    commitsText: "feat: a",
    language: "zh-cn",
    titlePrompt: "",
    descriptionPrompt: "  "
  });
  assert.doesNotMatch(messages[1]!.content, /Title prompt tuning:/);
  assert.doesNotMatch(messages[1]!.content, /Description prompt tuning:/);
});

test("resolvePullRequestTitle keeps generated title when titlePrompt is set", async () => {
  const { resolvePullRequestTitle } = await import("../pullRequest/hostAgentContent");
  assert.equal(
    resolvePullRequestTitle("feat: add remote compare", "LLM generated title", "Keep it short"),
    "LLM generated title"
  );
});
```

Also add a `resolveHostAgentPullRequestContent` test: single commit + `titlePrompt: "Keep it short"` + mocked complete returning `LLM generated title` → result title is `LLM generated title`.

Update existing `buildPullRequestContentMessages includes branches and schema` to pass `titlePrompt: ""` and `descriptionPrompt: ""` if the type requires them.

- [ ] **Step 2: Run tests to verify they fail**

Run: `npm test -- --test-name-pattern="prompt tuning|titlePrompt is set"`

Expected: FAIL.

- [ ] **Step 3: Implement**

`src/pullRequest/contentPrompt.ts` — add `titlePrompt` and `descriptionPrompt` to the input interface. Copy `buildPromptTuningSection` from CLI `src/pullRequest/contentPrompt.ts` and splice it into the user prompt **before** `Changed files`, matching CLI order.

`src/pullRequest/hostAgentContent.ts`:

```ts
export function resolvePullRequestTitle(
  commitsText: string,
  generatedTitle: string,
  titlePrompt: string
): string {
  if (titlePrompt.trim()) {
    return generatedTitle;
  }
  const subjects = parseCommitSubjects(commitsText);
  if (subjects.length !== 1) {
    return generatedTitle;
  }
  return subjects[0] || generatedTitle;
}
```

Remove the old comment about host-agent having no `titlePrompt` field.

`resolveHostAgentPullRequestContent` input adds `titlePrompt?: string` and `descriptionPrompt?: string` (default `""`). Pass them into `buildPullRequestContentMessages`. Generated path:

```ts
    title: resolvePullRequestTitle(input.commitsText, parsed.title, input.titlePrompt ?? ""),
```

- [ ] **Step 4: Run tests**

Run: `npm test -- --test-name-pattern="prContent|buildPullRequestContentMessages|resolvePullRequestTitle|resolveHostAgentPullRequestContent"`

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/pullRequest/contentPrompt.ts src/pullRequest/hostAgentContent.ts src/test/prContent.test.ts
git commit -m "feat: inject PR title and description prompt tuning"
```

---

### Task 5: Create API reviewers, milestone, draft

**Files:**
- Modify: `src/pullRequest/createApi.ts`
- Test: `src/test/createApi.test.ts`

- [ ] **Step 1: Extend `PullRequestCreationApiConfig` usage in tests**

Update `baseConfig` in `src/test/createApi.test.ts`:

```ts
const baseConfig = {
  authToken: "token-value",
  assignees: [] as string[],
  reviewers: [] as string[],
  labels: [] as string[],
  milestone: "",
  removeSourceBranch: false,
  draft: false
};
```

Add tests (port behavior from CLI `src/pullRequest/api.ts`):

```ts
test("createPullRequest sends GitHub draft reviewers and milestone", async () => {
  const seen: Array<{ url: string; method?: string; body: Record<string, unknown> }> = [];
  await withMockedFetch(async (url, init) => {
    const body = JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>;
    seen.push({ url, method: init?.method, body });
    if (url.endsWith("/pulls") && init?.method === "POST") {
      return new Response(JSON.stringify({ number: 3, title: "Add feature", html_url: "https://github.com/acme/demo/pull/3" }), { status: 201 });
    }
    if (url.includes("/requested_reviewers")) {
      return new Response("{}", { status: 201 });
    }
    if (url.includes("/issues/3") && init?.method === "PATCH") {
      return new Response("{}", { status: 200 });
    }
    return new Response("{}", { status: 200 });
  }, async () => {
    const created = await createPullRequest({
      platform: githubPlatform,
      config: {
        ...baseConfig,
        draft: true,
        reviewers: ["alice", "acme/reviewers"],
        milestone: "42"
      },
      sourceBranch: "feature/x",
      targetBranch: "main",
      timeoutMs: 5000,
      content: { title: "Add feature", description: "body" }
    });
    assert.equal(created.url, "https://github.com/acme/demo/pull/3");
    const createBody = seen.find((item) => item.url.endsWith("/pulls"))?.body;
    assert.equal(createBody?.draft, true);
    const reviewerCall = seen.find((item) => item.url.includes("/requested_reviewers"));
    assert.deepEqual(reviewerCall?.body.reviewers, ["alice"]);
    assert.deepEqual(reviewerCall?.body.team_reviewers, ["reviewers"]);
    const milestoneCall = seen.find((item) => item.method === "PATCH");
    assert.equal(milestoneCall?.body.milestone, 42);
  });
});

test("createPullRequest records GitHub reviewer failures as warnings", async () => {
  await withMockedFetch(async (url, init) => {
    if (url.endsWith("/pulls") && init?.method === "POST") {
      return new Response(JSON.stringify({ number: 3, title: "Add feature", html_url: "https://github.com/acme/demo/pull/3" }), { status: 201 });
    }
    if (url.includes("/requested_reviewers")) {
      return new Response("nope", { status: 422 });
    }
    return new Response("{}", { status: 200 });
  }, async () => {
    const created = await createPullRequest({
      platform: githubPlatform,
      config: { ...baseConfig, reviewers: ["alice"] },
      sourceBranch: "feature/x",
      targetBranch: "main",
      timeoutMs: 5000,
      content: { title: "Add feature", description: "body" }
    });
    assert.equal(created.warnings.length, 1);
    assert.match(created.warnings[0]!, /GitHub reviewers/);
  });
});

test("createPullRequest sends GitLab draft reviewer_ids and milestone_id", async () => {
  await withMockedFetch(async (url, init) => {
    if (url.includes("/users?")) {
      return new Response(JSON.stringify([{ id: 7, username: "alice" }]), { status: 200 });
    }
    if (url.includes("/milestones?")) {
      return new Response(JSON.stringify([{ id: 9, title: "Sprint" }]), { status: 200 });
    }
    if (url.endsWith("/merge_requests") && init?.method === "POST") {
      const body = JSON.parse(String(init.body ?? "{}")) as Record<string, unknown>;
      assert.equal(body.draft, true);
      assert.deepEqual(body.reviewer_ids, [7]);
      assert.equal(body.milestone_id, 9);
      return new Response(JSON.stringify({ title: "Add app", web_url: "https://gitlab.com/group/project/-/merge_requests/42" }), { status: 201 });
    }
    throw new Error(`unexpected ${init?.method} ${url}`);
  }, async () => {
    const created = await createPullRequest({
      platform: gitlabPlatform,
      config: {
        ...baseConfig,
        draft: true,
        reviewers: ["alice"],
        milestone: "Sprint"
      },
      sourceBranch: "feature/x",
      targetBranch: "main",
      timeoutMs: 5000,
      content: { title: "Add app", description: "## Summary" }
    });
    assert.equal(created.url, "https://gitlab.com/group/project/-/merge_requests/42");
  });
});

test("createPullRequest throws when GitLab reviewer is missing", async () => {
  await withMockedFetch(async (url) => {
    if (url.includes("/users?")) {
      return new Response("[]", { status: 200 });
    }
    throw new Error(`unexpected ${url}`);
  }, async () => {
    await assert.rejects(
      () =>
        createPullRequest({
          platform: gitlabPlatform,
          config: { ...baseConfig, reviewers: ["missing"] },
          sourceBranch: "feature/x",
          targetBranch: "main",
          timeoutMs: 5000,
          content: { title: "Add app", description: "## Summary" }
        }),
      /GitLab reviewer not found: missing/
    );
  });
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `npm test -- --test-name-pattern="createPullRequest sends GitHub|GitHub reviewer failures|GitLab draft|GitLab reviewer is missing"`

Expected: FAIL — config type missing fields / API not sending reviewers.

- [ ] **Step 3: Port API from CLI `src/pullRequest/api.ts`**

Update `PullRequestCreationApiConfig`:

```ts
export interface PullRequestCreationApiConfig {
  authToken: string;
  assignees: string[];
  reviewers: string[];
  labels: string[];
  milestone: string;
  removeSourceBranch: boolean;
  draft: boolean;
}
```

GitHub `createGitHubPullRequest`: after labels, call `updateGitHubReviewers` and `updateGitHubMilestone` via existing `collectWarning` (copy the two functions and `resolveGitHubMilestoneNumber` from CLI). Team reviewers: if the string includes `/`, push `split("/").filter(Boolean).pop() ?? reviewer` into `team_reviewers`.

GitLab `createGitLabMergeRequest`:

```ts
  const [assigneeIds, reviewerIds, milestoneId] = await Promise.all([
    resolveGitLabUserIds(input, headers, input.config.assignees, "assignee"),
    resolveGitLabUserIds(input, headers, input.config.reviewers, "reviewer"),
    resolveGitLabMilestoneId(input, headers, input.config.milestone)
  ]);
```

POST body adds `reviewer_ids: reviewerIds` and `milestone_id: milestoneId`. Copy `resolveGitLabMilestoneId` from CLI (empty → `undefined`; digits → `Number`; else search active milestones by title and throw if missing).

Add `GitHubMilestone` / `GitLabMilestone` interfaces as in CLI.

Empty reviewers / empty milestone remain no-ops (GitHub skips extra POSTs; GitLab `resolveGitLabUserIds` returns `[]`, milestone helper returns `undefined`, `JSON.stringify` omits `undefined`).

- [ ] **Step 4: Run tests**

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

Expected: PASS. Then `npm test`.

- [ ] **Step 5: Commit**

```bash
git add src/pullRequest/createApi.ts src/test/createApi.test.ts
git commit -m "feat: send PR reviewers, milestone, and draft to GitHub and GitLab"
```

---

### Task 6: Wire bridge and pull-request create

**Files:**
- Modify: `src/commands/bridge.ts`, `src/commands/pullRequestCreate.ts`
- Test: `src/test/pullRequestCreate.test.ts` (and `src/test/bridgeFull.test.ts` if it constructs `apiConfig` / assumes hardcoded draft)

- [ ] **Step 1: Write a failing command test** in `src/test/pullRequestCreate.test.ts`

Add a test that config `draft: true`, `reviewers: ["alice"]`, `titlePrompt: "Use product title"`:

1. First run returns `needs_host_agent`.
2. Write turn response `{"title":"LLM title","description":"LLM body"}`.
3. Mock fetch: GitLab users `alice` → id 7; POST merge_requests body has `draft: true`, `reviewer_ids: [7]`.
4. Assert created.
5. Because the fixture repo has **one** feature commit, without `titlePrompt` the title would be `feat: add app`. With `titlePrompt` set, title must stay `LLM title`.

Follow the existing `writeTurnResponse` / `withMockedFetch` / `writeConfig` helpers in that file. `writeConfig` already spreads `override.pullRequestCreation`.

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

Run: `npm test -- --test-name-pattern="draft reviewers titlePrompt"`

Expected: FAIL — `draft: false` hardcoded; prompts not passed; title overwritten by commit subject.

- [ ] **Step 3: Wire callers**

In `src/commands/pullRequestCreate.ts` and `src/commands/bridge.ts`, build `apiConfig` as:

```ts
    const apiConfig = {
      authToken,
      assignees: [...config.pullRequestCreation.assignees],
      reviewers: [...config.pullRequestCreation.reviewers],
      labels: [...config.pullRequestCreation.labels],
      milestone: config.pullRequestCreation.milestone,
      removeSourceBranch: config.pullRequestCreation.removeSourceBranch,
      draft: config.pullRequestCreation.draft
    };
```

Pass prompts into `resolveHostAgentPullRequestContent`:

```ts
      titlePrompt: config.pullRequestCreation.titlePrompt,
      descriptionPrompt: config.pullRequestCreation.descriptionPrompt,
```

Do not leave `draft: false` anywhere in these two files.

- [ ] **Step 4: Run tests**

Run: `npm test -- --test-name-pattern="pullRequestCreate|bridgeFull|draft reviewers"`

Expected: PASS. Then `npm test`.

- [ ] **Step 5: Commit**

```bash
git add src/commands/bridge.ts src/commands/pullRequestCreate.ts src/test/pullRequestCreate.test.ts src/test/bridgeFull.test.ts
git commit -m "feat: honor PR creation prompts, reviewers, milestone, and draft in commands"
```

---

### Task 7: Docs, contracts, changelog

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

- [ ] **Step 1: Expand `src/contracts.ts`**

Replace `pullRequestCreation: { type: "object" }` with an explicit object (mirror CLI `pullRequestCreationConfigSchema` field names). Add `configFilePath` to `pullRequestReview` properties + `required`.

```ts
    pullRequestCreation: {
      type: "object",
      additionalProperties: false,
      properties: {
        autoCreateAfterPush: booleanSchema,
        configFilePath: stringSchema,
        targetBranch: stringSchema,
        titlePrompt: stringSchema,
        descriptionPrompt: stringSchema,
        maxDiffChars: numberSchema,
        assignees: { type: "array", items: stringSchema },
        reviewers: { type: "array", items: stringSchema },
        labels: { type: "array", items: stringSchema },
        milestone: stringSchema,
        draft: booleanSchema,
        removeSourceBranch: booleanSchema,
        skipBranches: { type: "array", items: stringSchema }
      },
      required: [
        "autoCreateAfterPush",
        "configFilePath",
        "targetBranch",
        "titlePrompt",
        "descriptionPrompt",
        "maxDiffChars",
        "assignees",
        "reviewers",
        "labels",
        "milestone",
        "draft",
        "removeSourceBranch",
        "skipBranches"
      ]
    },
```

Add `"configFilePath"` to `pullRequestReview.properties` and `required`.

If `src/test` has schema-print assertions, update them.

- [ ] **Step 2: Docs**

`docs/configuration.md`:

- Delete the paragraph that `smartCommitCli` is accepted for migration.
- State: config files (including overlays) must use root key `smartCommitHostAgent` only.
- Add rows to `pullRequestCreation.*` for the six fields (copy wording from CLI `docs/configuration.md` where it matches).
- Add `pullRequestReview.configFilePath`.
- Document overlay: comma-separated list, first existing file, relative to `--repo` or cwd, creation overlay forbids `configFilePath` / `autoCreateAfterPush`, review overlay forbids `configFilePath`.

`docs/parity-matrix.md`:

- `bridge` / `pull-request create` notes: creation fields + overlay aligned; still no PR content repair.

`README.md`:

- Remove “Migrating a smart-commit-cli config file” section (or replace with: rename the root key to `smartCommitHostAgent`; `connection` is still stripped if present inside it).
- Comparison table: Config root key is `smartCommitHostAgent` only.
- Example `pullRequestCreation` may include `titlePrompt` / `reviewers` / `configFilePath: ""`.

`examples/config.host-agent.json`: optional `titlePrompt` / `reviewers` empty defaults are enough; do not add a real overlay path.

`CHANGELOG.md` `[Unreleased]`:

```md
### Added
- `pullRequestCreation.configFilePath`, `titlePrompt`, `descriptionPrompt`, `reviewers`, `milestone`, `draft`
- `pullRequestReview.configFilePath` overlay loading

### Changed
- Config files and overlays only accept root key `smartCommitHostAgent` (`smartCommitCli` is rejected)
- `bridge` / `pull-request create` send reviewers, milestone, and draft to GitHub/GitLab
- Non-empty `titlePrompt` skips single-commit subject reuse for the PR title
```

Do **not** edit historical Plan 4 / Plan 6 / foundation docs.

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

Run: `npm test`

Expected: PASS.

```bash
git add docs/configuration.md docs/parity-matrix.md README.md examples/config.host-agent.json src/contracts.ts CHANGELOG.md
git commit -m "docs: document PR creation CLI parity and smartCommitHostAgent-only root"
```

---

## Self-review (spec coverage)

| Spec section | Task |
|--------------|------|
| §3 fields, defaults, parse, merge | Task 1 |
| §4 root key | Task 2 |
| §5 overlay | Task 3 |
| §6 prompt / title | Task 4 |
| §7 create API | Task 5 |
| wire bridge / pull-request create | Task 6 |
| §8 docs / contracts / CHANGELOG | Task 7 |
| §9 tests | Tasks 1–6 |
| §10 breaking changes | Tasks 2, 4, 6, 7 |
| Non-goals: no CLI flags/env, no repair, keep autoCreate/removeSource defaults | all tasks |

No placeholders left. Types: `PullRequestCreationApiConfig` gains `reviewers` / `milestone` / `draft` in Task 5; commands pass those names in Task 6. Overlay helpers take `parseCanonical` to avoid `load.ts` ↔ `overlay.ts` cycles.
