# Host-Agent Foundation 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:** Scaffold `smart-commit-host-agent` with turn-session protocol, LLM-free config resolve, and a CLI entry that can exit with `needs_host_agent` — no external LLM calls.

**Architecture:** Node 20+ TypeScript CLI (CommonJS, mirroring smart-commit-cli toolchain). All “brain” work goes through `HostAgentClient`, which writes turn request files and either returns an existing response or throws `NeedsHostAgentError` so the command exits with status `needs_host_agent` and exit code `10`. Config validation never requires `connection.*`.

**Tech Stack:** TypeScript 5.6, Node built-in `node:test`, `tsc`, no runtime deps initially (add later only when Plan 3+ needs them).

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

---

## File structure (Plan 1)

| Path | Responsibility |
|------|----------------|
| `package.json` | Package metadata, bin, scripts, `peerReference.cliVersion` |
| `tsconfig.json` / `tsconfig.build.json` | Compile test+src / publish out |
| `src/cli.ts` | Process entry, exit codes |
| `src/cliApp.ts` | Parse argv, dispatch sync/async commands |
| `src/exitCodes.ts` | Shared exit code constants |
| `src/hostAgent/types.ts` | Turn request/response/session types |
| `src/hostAgent/sessionStore.ts` | Create session dir, read/write turns |
| `src/hostAgent/client.ts` | `HostAgentClient.complete` / `review` |
| `src/hostAgent/needsHostAgentError.ts` | Signal error + payload helpers |
| `src/config/schema.ts` | Config types **without** required LLM connection |
| `src/config/defaults.ts` | Default config values |
| `src/config/load.ts` | Load JSON file + `env:VAR` resolution for tokens |
| `src/config/resolve.ts` | Merge file + argv + env; validate |
| `src/commands/configResolve.ts` | `config resolve` command |
| `src/test/*.test.ts` | Unit tests |
| `docs/parity-matrix.md` | Capability matrix stub |
| `README.md` | Usage for Plan 1 commands |

---

### Task 1: Package scaffold

**Files:**
- Create: `package.json`
- Create: `tsconfig.json`
- Create: `tsconfig.build.json`
- Create: `.gitignore`
- Modify: `README.md`

- [ ] **Step 1: Create `package.json`**

```json
{
  "name": "smart-commit-host-agent",
  "version": "0.1.0",
  "private": false,
  "description": "Host-Agent orchestrated smart-commit workflows without LLM connection config.",
  "license": "MIT",
  "type": "commonjs",
  "engines": {
    "node": ">=20.0.0"
  },
  "bin": {
    "smart-commit-host-agent": "./out/cli.js"
  },
  "files": [
    "out",
    "README.md",
    "docs",
    "CHANGELOG.md"
  ],
  "peerReference": {
    "cliPackage": "smart-commit-copilot-cli",
    "cliVersion": "0.1.21"
  },
  "scripts": {
    "clean": "rm -rf out",
    "build": "npm run clean && tsc -p ./tsconfig.build.json",
    "test": "npm run clean && tsc -p ./tsconfig.json && node --test --test-reporter=spec out/test/*.test.js",
    "check": "npm test",
    "prepack": "npm run build"
  },
  "devDependencies": {
    "@types/node": "^20.16.5",
    "typescript": "^5.6.2"
  }
}
```

- [ ] **Step 2: Create `tsconfig.json`**

```json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2022",
    "lib": ["ES2022"],
    "outDir": "out",
    "rootDir": "src",
    "sourceMap": true,
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}
```

- [ ] **Step 3: Create `tsconfig.build.json`**

```json
{
  "extends": "./tsconfig.json",
  "exclude": ["src/test/**"]
}
```

- [ ] **Step 4: Create `.gitignore`**

```
node_modules/
out/
*.tgz
.DS_Store
.smart-commit-host-agent/
```

- [ ] **Step 5: Update `README.md` to state Plan 1 scope**

Replace body with:

```markdown
# smart-commit-host-agent

Host-Agent workflows for smart-commit **without** LLM `connection` / API keys.

Paired with `bugfix-gitlab-mr-qax`. For self-configured LLM use `smart-commit-copilot-cli` instead.

- Spec: [docs/superpowers/specs/2026-08-10-host-agent-design.md](docs/superpowers/specs/2026-08-10-host-agent-design.md)
- Roadmap: [docs/superpowers/plans/2026-08-10-host-agent-roadmap.md](docs/superpowers/plans/2026-08-10-host-agent-roadmap.md)

## Current (Plan 1)

```bash
npm install
npm test
node out/cli.js config resolve --config ./examples/config.host-agent.json
```

Commands beyond `config resolve` / help / version land in later plans.
```

- [ ] **Step 6: Install and verify TypeScript runs**

Run:

```bash
cd /Users/nietao/VSCode-plugins/smart-commit-host-agent
npm install
```

Expected: `node_modules/typescript` present; no errors.

- [ ] **Step 7: Commit**

```bash
git add package.json package-lock.json tsconfig.json tsconfig.build.json .gitignore README.md
git commit -m "$(cat <<'EOF'
chore: scaffold smart-commit-host-agent package

EOF
)"
```

---

### Task 2: Exit codes and NeedsHostAgentError

**Files:**
- Create: `src/exitCodes.ts`
- Create: `src/hostAgent/needsHostAgentError.ts`
- Create: `src/test/needsHostAgentError.test.ts`

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

Create `src/test/needsHostAgentError.test.ts`:

```typescript
import assert from "node:assert/strict";
import test from "node:test";
import { EXIT_CODE_NEEDS_HOST_AGENT } from "../exitCodes";
import { NeedsHostAgentError, isNeedsHostAgentError } from "../hostAgent/needsHostAgentError";

test("NeedsHostAgentError carries session paths and turnId", () => {
  const error = new NeedsHostAgentError({
    sessionPath: "/tmp/session",
    requestPath: "/tmp/session/turns/0001.request.json",
    turnId: "0001",
    purpose: "code-review"
  });

  assert.equal(error.name, "NeedsHostAgentError");
  assert.equal(error.sessionPath, "/tmp/session");
  assert.equal(error.requestPath, "/tmp/session/turns/0001.request.json");
  assert.equal(error.turnId, "0001");
  assert.equal(error.purpose, "code-review");
  assert.equal(isNeedsHostAgentError(error), true);
  assert.equal(EXIT_CODE_NEEDS_HOST_AGENT, 10);
});
```

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

Run: `npm test`  
Expected: FAIL (modules missing / compile error).

- [ ] **Step 3: Implement exit codes and error**

`src/exitCodes.ts`:

```typescript
export const EXIT_CODE_SUCCESS = 0;
export const EXIT_CODE_BLOCKED = 2;
export const EXIT_CODE_CONFIG_ERROR = 3;
export const EXIT_CODE_RUNTIME_ERROR = 4;
export const EXIT_CODE_NEEDS_HOST_AGENT = 10;
```

`src/hostAgent/needsHostAgentError.ts`:

```typescript
export interface NeedsHostAgentPayload {
  sessionPath: string;
  requestPath: string;
  turnId: string;
  purpose: string;
}

export class NeedsHostAgentError extends Error {
  public readonly sessionPath: string;
  public readonly requestPath: string;
  public readonly turnId: string;
  public readonly purpose: string;

  public constructor(payload: NeedsHostAgentPayload) {
    super(`Host agent response required for turn ${payload.turnId} (${payload.purpose}).`);
    this.name = "NeedsHostAgentError";
    this.sessionPath = payload.sessionPath;
    this.requestPath = payload.requestPath;
    this.turnId = payload.turnId;
    this.purpose = payload.purpose;
  }
}

export function isNeedsHostAgentError(error: unknown): error is NeedsHostAgentError {
  return error instanceof NeedsHostAgentError;
}
```

- [ ] **Step 4: Run tests and ensure they pass**

Run: `npm test`  
Expected: PASS for `needsHostAgentError` test.

- [ ] **Step 5: Commit**

```bash
git add src/exitCodes.ts src/hostAgent/needsHostAgentError.ts src/test/needsHostAgentError.test.ts
git commit -m "$(cat <<'EOF'
feat: add needs_host_agent error and exit code 10

EOF
)"
```

---

### Task 3: Session store (turn request/response files)

**Files:**
- Create: `src/hostAgent/types.ts`
- Create: `src/hostAgent/sessionStore.ts`
- Create: `src/test/sessionStore.test.ts`

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

```typescript
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { createSessionStore } from "../hostAgent/sessionStore";

test("sessionStore writes request and reads matching response", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "scha-session-"));
  try {
    const store = createSessionStore({
      baseDir: root,
      command: "bridge",
      repositoryPath: "/repo",
      cliReferenceVersion: "0.1.21"
    });

    assert.ok(fs.existsSync(path.join(store.sessionPath, "session.json")));

    const written = store.writeRequest({
      kind: "review",
      purpose: "code-review",
      messages: [{ role: "user", content: "review this" }],
      responseSchema: "JSON review payload",
      attempt: 0
    });

    assert.equal(written.turnId, "0001");
    assert.ok(fs.existsSync(written.requestPath));

    assert.equal(store.readResponse("0001"), null);

    const responsePath = path.join(store.sessionPath, "turns", "0001.response.json");
    fs.writeFileSync(
      responsePath,
      JSON.stringify({ turnId: "0001", content: "{\"score\":9}" }),
      "utf8"
    );

    assert.equal(store.readResponse("0001"), "{\"score\":9}");
  } finally {
    fs.rmSync(root, { recursive: true, force: true });
  }
});

test("sessionStore rejects response with mismatched turnId", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "scha-session-"));
  try {
    const store = createSessionStore({
      baseDir: root,
      command: "bridge",
      repositoryPath: "/repo",
      cliReferenceVersion: "0.1.21"
    });
    store.writeRequest({
      kind: "complete",
      purpose: "commit-message",
      messages: [{ role: "user", content: "msg" }],
      responseSchema: "text",
      attempt: 0
    });
    fs.writeFileSync(
      path.join(store.sessionPath, "turns", "0001.response.json"),
      JSON.stringify({ turnId: "9999", content: "x" }),
      "utf8"
    );
    assert.throws(() => store.readResponse("0001"), /turnId/);
  } finally {
    fs.rmSync(root, { recursive: true, force: true });
  }
});
```

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

Run: `npm test`  
Expected: FAIL (missing `createSessionStore`).

- [ ] **Step 3: Implement types and session store**

`src/hostAgent/types.ts`:

```typescript
export type HostAgentMessageRole = "system" | "user" | "assistant";

export interface HostAgentChatMessage {
  role: HostAgentMessageRole;
  content: string;
}

export type HostAgentTurnKind = "complete" | "review";

export interface HostAgentTurnRequest {
  turnId: string;
  kind: HostAgentTurnKind;
  purpose: string;
  messages: HostAgentChatMessage[];
  responseSchema: string;
  attempt: number;
}

export interface HostAgentTurnResponse {
  turnId: string;
  content: string;
}

export interface HostAgentSessionMeta {
  sessionId: string;
  command: string;
  repositoryPath: string;
  createdAt: string;
  cliReferenceVersion: string;
}
```

`src/hostAgent/sessionStore.ts`:

```typescript
import fs from "node:fs";
import path from "node:path";
import { randomUUID } from "node:crypto";
import type {
  HostAgentChatMessage,
  HostAgentSessionMeta,
  HostAgentTurnKind,
  HostAgentTurnRequest,
  HostAgentTurnResponse
} from "./types";

export interface CreateSessionStoreInput {
  baseDir: string;
  command: string;
  repositoryPath: string;
  cliReferenceVersion: string;
  sessionId?: string;
}

export interface SessionStore {
  sessionPath: string;
  sessionId: string;
  writeRequest(input: {
    kind: HostAgentTurnKind;
    purpose: string;
    messages: HostAgentChatMessage[];
    responseSchema: string;
    attempt: number;
  }): { turnId: string; requestPath: string; request: HostAgentTurnRequest };
  readResponse(turnId: string): string | null;
  nextTurnId(): string;
}

export function openExistingSession(sessionPath: string): SessionStore {
  const metaPath = path.join(sessionPath, "session.json");
  if (!fs.existsSync(metaPath)) {
    throw new Error(`Session not found: ${sessionPath}`);
  }
  const meta = JSON.parse(fs.readFileSync(metaPath, "utf8")) as HostAgentSessionMeta;
  return buildStore(sessionPath, meta.sessionId);
}

export function createSessionStore(input: CreateSessionStoreInput): SessionStore {
  const sessionId = input.sessionId ?? randomUUID();
  const sessionPath = path.join(input.baseDir, sessionId);
  fs.mkdirSync(path.join(sessionPath, "turns"), { recursive: true });
  const meta: HostAgentSessionMeta = {
    sessionId,
    command: input.command,
    repositoryPath: input.repositoryPath,
    createdAt: new Date().toISOString(),
    cliReferenceVersion: input.cliReferenceVersion
  };
  fs.writeFileSync(path.join(sessionPath, "session.json"), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
  return buildStore(sessionPath, sessionId);
}

function buildStore(sessionPath: string, sessionId: string): SessionStore {
  return {
    sessionPath,
    sessionId,
    nextTurnId(): string {
      const turnsDir = path.join(sessionPath, "turns");
      const requests = fs
        .readdirSync(turnsDir)
        .filter((name) => name.endsWith(".request.json"))
        .map((name) => Number.parseInt(name.slice(0, 4), 10))
        .filter((n) => Number.isFinite(n));
      const next = (requests.length === 0 ? 1 : Math.max(...requests) + 1);
      return String(next).padStart(4, "0");
    },
    writeRequest(input) {
      const turnId = this.nextTurnId();
      const request: HostAgentTurnRequest = {
        turnId,
        kind: input.kind,
        purpose: input.purpose,
        messages: input.messages,
        responseSchema: input.responseSchema,
        attempt: input.attempt
      };
      const requestPath = path.join(sessionPath, "turns", `${turnId}.request.json`);
      fs.writeFileSync(requestPath, `${JSON.stringify(request, null, 2)}\n`, "utf8");
      return { turnId, requestPath, request };
    },
    readResponse(turnId: string): string | null {
      const responsePath = path.join(sessionPath, "turns", `${turnId}.response.json`);
      if (!fs.existsSync(responsePath)) {
        return null;
      }
      const parsed = JSON.parse(fs.readFileSync(responsePath, "utf8")) as HostAgentTurnResponse;
      if (parsed.turnId !== turnId) {
        throw new Error(`Response turnId mismatch: expected ${turnId}, got ${parsed.turnId}`);
      }
      if (typeof parsed.content !== "string") {
        throw new Error(`Response content must be a string for turn ${turnId}.`);
      }
      return parsed.content;
    }
  };
}
```

- [ ] **Step 4: Run tests and ensure they pass**

Run: `npm test`  
Expected: sessionStore tests PASS.

- [ ] **Step 5: Commit**

```bash
git add src/hostAgent/types.ts src/hostAgent/sessionStore.ts src/test/sessionStore.test.ts
git commit -m "$(cat <<'EOF'
feat: add host-agent turn session store

EOF
)"
```

---

### Task 4: HostAgentClient

**Files:**
- Create: `src/hostAgent/client.ts`
- Create: `src/test/hostAgentClient.test.ts`

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

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

test("HostAgentClient throws NeedsHostAgentError when response missing", async () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "scha-client-"));
  try {
    const store = createSessionStore({
      baseDir: root,
      command: "bridge",
      repositoryPath: "/repo",
      cliReferenceVersion: "0.1.21"
    });
    const client = createHostAgentClient({ store });

    await assert.rejects(
      () =>
        client.review([{ role: "user", content: "diff" }], {
          purpose: "code-review",
          responseSchema: "json"
        }),
      (error: unknown) => isNeedsHostAgentError(error)
    );
  } finally {
    fs.rmSync(root, { recursive: true, force: true });
  }
});

test("HostAgentClient returns content when response exists for pending turn", async () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "scha-client-"));
  try {
    const store = createSessionStore({
      baseDir: root,
      command: "bridge",
      repositoryPath: "/repo",
      cliReferenceVersion: "0.1.21"
    });
    const pending = store.writeRequest({
      kind: "review",
      purpose: "code-review",
      messages: [{ role: "user", content: "diff" }],
      responseSchema: "json",
      attempt: 0
    });
    fs.writeFileSync(
      path.join(store.sessionPath, "turns", `${pending.turnId}.response.json`),
      JSON.stringify({ turnId: pending.turnId, content: "ok-review" }),
      "utf8"
    );

    const client = createHostAgentClient({ store });
    const text = await client.review([{ role: "user", content: "diff" }], {
      purpose: "code-review",
      responseSchema: "json"
    });
    assert.equal(text, "ok-review");
  } finally {
    fs.rmSync(root, { recursive: true, force: true });
  }
});
```

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

Run: `npm test`  
Expected: FAIL (missing client).

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

Behavior:

1. Look for the latest request with same `purpose`+`kind` that has a response → if calling again with same messages hash is complex; **Plan 1 simpler rule**:  
   - On `complete`/`review`, if there is an **unanswered** latest request for this purpose, wait—no: on continue, response was written for the last request.  
   - **Algorithm:**  
     a. Find highest turnId whose request exists.  
     b. If that turn has **no** response → this invocation is the same pending turn only when messages match; Plan 1 uses: if unanswered request exists for purpose, **do not write a new request**; throw `NeedsHostAgentError` again pointing at it.  
     c. If unanswered request exists and response now present → return content.  
     d. If all requests answered (or none) → write new request and throw `NeedsHostAgentError`.

`src/hostAgent/client.ts`:

```typescript
import fs from "node:fs";
import path from "node:path";
import { NeedsHostAgentError } from "./needsHostAgentError";
import type { SessionStore } from "./sessionStore";
import type { HostAgentChatMessage, HostAgentTurnKind, HostAgentTurnRequest } from "./types";

export interface HostAgentCallOptions {
  purpose: string;
  responseSchema: string;
  attempt?: number;
}

export interface HostAgentClient {
  complete(messages: HostAgentChatMessage[], options: HostAgentCallOptions): Promise<string>;
  review(messages: HostAgentChatMessage[], options: HostAgentCallOptions): Promise<string>;
}

export function createHostAgentClient(input: { store: SessionStore }): HostAgentClient {
  const { store } = input;

  async function call(kind: HostAgentTurnKind, messages: HostAgentChatMessage[], options: HostAgentCallOptions): Promise<string> {
    const pending = findLatestRequest(store.sessionPath, kind, options.purpose);
    if (pending) {
      const content = store.readResponse(pending.turnId);
      if (content !== null) {
        return content;
      }
      throw new NeedsHostAgentError({
        sessionPath: store.sessionPath,
        requestPath: path.join(store.sessionPath, "turns", `${pending.turnId}.request.json`),
        turnId: pending.turnId,
        purpose: options.purpose
      });
    }

    const written = store.writeRequest({
      kind,
      purpose: options.purpose,
      messages,
      responseSchema: options.responseSchema,
      attempt: options.attempt ?? 0
    });
    throw new NeedsHostAgentError({
      sessionPath: store.sessionPath,
      requestPath: written.requestPath,
      turnId: written.turnId,
      purpose: options.purpose
    });
  }

  return {
    complete: (messages, options) => call("complete", messages, options),
    review: (messages, options) => call("review", messages, options)
  };
}

function findLatestRequest(
  sessionPath: string,
  kind: HostAgentTurnKind,
  purpose: string
): HostAgentTurnRequest | null {
  const turnsDir = path.join(sessionPath, "turns");
  if (!fs.existsSync(turnsDir)) {
    return null;
  }
  const files = fs
    .readdirSync(turnsDir)
    .filter((name) => name.endsWith(".request.json"))
    .sort();
  for (let i = files.length - 1; i >= 0; i -= 1) {
    const request = JSON.parse(
      fs.readFileSync(path.join(turnsDir, files[i]!), "utf8")
    ) as HostAgentTurnRequest;
    if (request.kind === kind && request.purpose === purpose) {
      return request;
    }
  }
  return null;
}
```

- [ ] **Step 4: Run tests and ensure they pass**

Run: `npm test`  
Expected: hostAgentClient tests PASS.

- [ ] **Step 5: Commit**

```bash
git add src/hostAgent/client.ts src/test/hostAgentClient.test.ts
git commit -m "$(cat <<'EOF'
feat: add HostAgentClient turn complete/review API

EOF
)"
```

---

### Task 5: LLM-free config schema + load + resolve

**Files:**
- Create: `src/config/schema.ts`
- Create: `src/config/defaults.ts`
- Create: `src/config/load.ts`
- Create: `src/config/resolve.ts`
- Create: `examples/config.host-agent.json`
- Create: `src/test/configResolve.test.ts`

Reference (read-only): `smart-commit-cli/src/config/schema.ts` — copy **non-connection** fields only; do **not** require `baseUrl`/`apiKey`/`model`.

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

```typescript
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { resolveHostAgentConfig } from "../config/resolve";

test("resolveHostAgentConfig succeeds without connection LLM fields", () => {
  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: 6, language: "zh-cn" },
        pullRequest: {
          provider: "gitlab",
          authToken: "env:SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN"
        }
      }
    }),
    "utf8"
  );

  const resolved = resolveHostAgentConfig({
    configPath,
    env: { SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN: "glpat-test-token" }
  });

  assert.equal(resolved.config.review.threshold, 6);
  assert.equal(resolved.config.pullRequest.authToken, "glpat-test-token");
  assert.equal("connection" in resolved.config, false);
});

test("resolveHostAgentConfig fails when pullRequest authToken env missing", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cfg-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        pullRequest: { authToken: "env:SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN" }
      }
    }),
    "utf8"
  );

  assert.throws(
    () => resolveHostAgentConfig({ configPath, env: {} }),
    /SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN/
  );
});
```

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

Run: `npm test`  
Expected: FAIL.

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

Keep Plan 1 schema lean (expand in later plans as commands need fields):

`src/config/schema.ts`:

```typescript
export interface HostAgentConfig {
  review: {
    threshold: number;
    language: string;
    maxDiffChars: number;
  };
  commitMessage: {
    language: string;
    autoGenerate: boolean;
    validation: {
      protocol: "conventional" | "none" | "custom";
      pattern: string;
      extractTicketIdFromBranch: boolean;
      requireTicketIdInMessage: boolean;
    };
  };
  git: {
    autoStageWhenNothingStaged: boolean;
    autoCommit: boolean;
    autoPush: boolean;
    pushTimeoutMs: number;
  };
  pullRequest: {
    provider: "auto" | "github" | "gitlab";
    apiBaseUrl: string;
    authToken: string;
  };
  pullRequestCreation: {
    autoCreateAfterPush: boolean;
    targetBranch: string;
    maxDiffChars: number;
    assignees: string[];
    labels: string[];
    removeSourceBranch: boolean;
    skipBranches: string[];
  };
  pullRequestReview: {
    threshold: number;
    autoApprove: boolean;
    autoMerge: boolean;
  };
  output: {
    format: "json" | "text";
    logLevel: "debug" | "info" | "warn" | "error";
  };
}

export interface ResolvedHostAgentConfig {
  config: HostAgentConfig;
  configPath: string | null;
}
```

`src/config/defaults.ts` — return full `HostAgentConfig` with safe defaults (threshold `6`, languages `zh-cn`, git autos true, empty token, provider `auto`, etc.).

`src/config/load.ts`:

- Read file; accept root key `smartCommitHostAgent` (preferred) or `smartCommitCli` (compat for migrating skill JSON by stripping connection later).
- Resolve strings matching `^env:([A-Z0-9_]+)$` via `env`.
- Deep-merge over defaults.

`src/config/resolve.ts`:

```typescript
export function resolveHostAgentConfig(input: {
  configPath?: string;
  env?: NodeJS.ProcessEnv;
  argv?: string[];
}): ResolvedHostAgentConfig
```

- Parse `--config <path>` from argv if `configPath` omitted.
- After merge, if `pullRequest.authToken` is empty string, throw Error explaining token required when validating for platform commands — **for Plan 1 `config resolve`**, validate token only if config references `env:` or non-empty expected; the second test covers missing env.
- **Never** validate LLM connection fields.
- If argv contains `--api-key` / `--base-url` / `--model` / `--llm-provider`, throw: `smart-commit-host-agent does not use LLM connection flags.`

Also write `examples/config.host-agent.json` matching the first test shape (use `env:SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN`).

- [ ] **Step 4: Run tests and ensure they pass**

Run: `npm test`  
Expected: config tests PASS.

- [ ] **Step 5: Commit**

```bash
git add src/config examples/config.host-agent.json src/test/configResolve.test.ts
git commit -m "$(cat <<'EOF'
feat: add LLM-free host-agent config resolve core

EOF
)"
```

---

### Task 6: `config resolve` command + CLI entry

**Files:**
- Create: `src/commands/configResolve.ts`
- Create: `src/cliApp.ts`
- Create: `src/cli.ts`
- Create: `src/test/cliApp.test.ts`

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

```typescript
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { runCliAsync } from "../cliApp";
import { EXIT_CODE_CONFIG_ERROR, EXIT_CODE_SUCCESS } from "../exitCodes";

test("config resolve returns redacted json without connection", async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scha-cli-"));
  const configPath = path.join(dir, "cfg.json");
  fs.writeFileSync(
    configPath,
    JSON.stringify({
      smartCommitHostAgent: {
        review: { threshold: 7 },
        pullRequest: { authToken: "env:SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN" }
      }
    }),
    "utf8"
  );

  const result = await runCliAsync(
    ["config", "resolve", "--config", configPath, "--output", "json"],
    { SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN: "secret-token" }
  );

  assert.equal(result.exitCode, EXIT_CODE_SUCCESS);
  const payload = JSON.parse(result.stdout);
  assert.equal(payload.status, "resolved");
  assert.equal(payload.config.review.threshold, 7);
  assert.equal(payload.config.pullRequest.authToken, "[REDACTED]");
  assert.equal(payload.config.connection, undefined);
});

test("config resolve rejects LLM flags", async () => {
  const result = await runCliAsync(["config", "resolve", "--api-key", "x"], {});
  assert.equal(result.exitCode, EXIT_CODE_CONFIG_ERROR);
  assert.match(result.stdout + result.stderr, /does not use LLM/i);
});
```

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

Run: `npm test`  
Expected: FAIL.

- [ ] **Step 3: Implement command + cliApp + cli**

`src/commands/configResolve.ts` — build payload:

```typescript
{
  schemaVersion: "1",
  status: "resolved",
  command: "config resolve",
  configPath: string | null,
  config: /* authToken replaced with [REDACTED] */,
  summary: "Configuration resolved."
}
```

On error: `status: "error"`, `error: { code: "CONFIG_ERROR", message }`, exit `3`.

`src/cliApp.ts`:

- `parseCliCommand(argv)` → `help` | `version` | `config-resolve` | `unknown`
- `runCli` for help/version; `runCliAsync` for config-resolve
- help text lists Plan 1 commands and notes later commands coming
- version reads `peerReference` + package version from adjacent `package.json` (use `path.join(__dirname, "..", "package.json")` when running from `out/`)

`src/cli.ts` — same pattern as CLI `src/cli.ts` (async main, write stdout/stderr, set `process.exitCode`). Shebang `#!/usr/bin/env node`.

Redaction helper inline in configResolve: clone config and set `pullRequest.authToken` to `[REDACTED]` when non-empty.

- [ ] **Step 4: Run tests and ensure they pass**

Run: `npm test`  
Expected: all PASS.

- [ ] **Step 5: Manual smoke**

```bash
SMART_COMMIT_PULL_REQUEST_AUTH_TOKEN=dummy npm run build
node out/cli.js config resolve --config examples/config.host-agent.json --output json
```

Expected: JSON with `[REDACTED]` token; exit 0.

- [ ] **Step 6: Commit**

```bash
git add src/commands/configResolve.ts src/cliApp.ts src/cli.ts src/test/cliApp.test.ts
git commit -m "$(cat <<'EOF'
feat: add config resolve CLI entry without LLM flags

EOF
)"
```

---

### Task 7: Parity matrix stub + demo needs_host_agent helper command

**Files:**
- Create: `docs/parity-matrix.md`
- Create: `src/commands/hostAgentProbe.ts` (dev/probe only for Plan 1)
- Modify: `src/cliApp.ts` to wire `host-agent probe`
- Create: `src/test/hostAgentProbe.test.ts`

Purpose: prove end-to-end turn exit without waiting for full bridge (Plan 2).

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

```typescript
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { runCliAsync } from "../cliApp";
import { EXIT_CODE_NEEDS_HOST_AGENT, EXIT_CODE_SUCCESS } from "../exitCodes";

test("host-agent probe exits needs_host_agent then completes after response", async () => {
  const base = fs.mkdtempSync(path.join(os.tmpdir(), "scha-probe-"));
  const first = await runCliAsync(
    ["host-agent", "probe", "--session-base", base, "--output", "json"],
    {}
  );
  assert.equal(first.exitCode, EXIT_CODE_NEEDS_HOST_AGENT);
  const payload = JSON.parse(first.stdout);
  assert.equal(payload.status, "needs_host_agent");
  assert.ok(payload.sessionPath);
  assert.ok(payload.requestPath);
  assert.equal(payload.turnId, "0001");

  const request = JSON.parse(fs.readFileSync(payload.requestPath, "utf8"));
  fs.writeFileSync(
    path.join(payload.sessionPath, "turns", "0001.response.json"),
    JSON.stringify({ turnId: "0001", content: "probe-ok" }),
    "utf8"
  );

  const second = await runCliAsync(
    ["host-agent", "probe", "--session", payload.sessionPath, "--output", "json"],
    {}
  );
  assert.equal(second.exitCode, EXIT_CODE_SUCCESS);
  const done = JSON.parse(second.stdout);
  assert.equal(done.status, "passed");
  assert.equal(done.content, "probe-ok");
  assert.equal(request.purpose, "probe");
});
```

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

Run: `npm test`  
Expected: FAIL.

- [ ] **Step 3: Implement probe command**

`host-agent probe`:

1. Open `--session` or `createSessionStore({ baseDir: --session-base || os.tmpdir()/scha-sessions, command: "host-agent probe", ...})`
2. `createHostAgentClient` → `complete([{role:user,content:"ping"}], { purpose: "probe", responseSchema: "plain text" })`
3. Catch `NeedsHostAgentError` → stdout JSON `{ status, sessionPath, requestPath, turnId, purpose, command }` exit 10
4. On success → `{ status: "passed", content, sessionPath }` exit 0

Wire in `cliApp.ts`.

Write `docs/parity-matrix.md` table with all §4 commands marked `未跟进` except `config resolve` / help / version / `host-agent probe (internal)` as `已对齐` or `Plan 1`.

- [ ] **Step 4: Run tests — expect PASS**

- [ ] **Step 5: Commit**

```bash
git add docs/parity-matrix.md src/commands/hostAgentProbe.ts src/cliApp.ts src/test/hostAgentProbe.test.ts
git commit -m "$(cat <<'EOF'
feat: add host-agent probe and parity matrix stub

EOF
)"
```

---

### Task 8: Plan 1 self-check

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

```bash
cd /Users/nietao/VSCode-plugins/smart-commit-host-agent && npm test
```

Expected: all tests PASS.

- [ ] **Step 2: Confirm smart-commit-cli untouched**

```bash
cd /Users/nietao/VSCode-plugins/smart-commit-cli && git status --porcelain
```

Expected: only user’s pre-existing local changes (if any); **no** new host-agent-related edits from this plan.

- [ ] **Step 3: Update roadmap checkboxes**

In `docs/superpowers/plans/2026-08-10-host-agent-roadmap.md`, note Plan 1 complete.

- [ ] **Step 4: Commit docs if changed**

```bash
git add docs/superpowers/plans/2026-08-10-host-agent-roadmap.md
git commit -m "$(cat <<'EOF'
docs: mark Plan 1 foundation complete on roadmap

EOF
)"
```

---

## Spec coverage (Plan 1)

| Spec section | Covered by |
|--------------|------------|
| §3 hard boundary: no CLI edits / no shared modules | Task 8 check; independent repo |
| §5 Turn protocol | Tasks 3–4, 7 |
| §7 `needs_host_agent` / exit 10 | Tasks 2, 7 |
| §9 no LLM connection; platform token | Task 5–6 |
| §8 matrix stub | Task 7 |
| §4 full commands | **Deferred** to Plans 2–5 (roadmap) |
| §6 skill migration | **Deferred** to Plan 5 |

## After Plan 1

Write **Plan 2** (`bridge --review-only` + review prompt/parser port) before implementing more commands.
