# Build Studio Decision Skill Packs 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:** Build the shared DPF skill-pack, decision-service, non-production environment, local integration CI, evidence-intake, founder-review, and simplified Build Studio UX substrate described in `docs/superpowers/specs/2026-05-26-build-studio-decision-skill-packs-design.md`.

**Architecture:** Extend the existing PR #1168 `packages/dpf-skill-pack` substrate first, then wire Build Studio to typed services instead of UI/orchestration branches. Runtime state uses existing evidence/decision models where they fit, plus a minimal dedicated lease model for active non-production environment ownership. Build Studio remains the simple convergence UI over decisions, environment status, and evidence.

**Tech Stack:** Next.js 16 App Router, TypeScript, Prisma 7, PostgreSQL, Vitest, DPF MCP `/api/mcp/v1`, `packages/dpf-skill-pack`, `scripts/dpf-compose.mjs`, Playwright/browser verification for UI slices.

---

## Source Design

- Spec: `docs/superpowers/specs/2026-05-26-build-studio-decision-skill-packs-design.md`
- Existing skill substrate: `packages/dpf-skill-pack/README.md`
- Existing seed loader: `packages/db/src/seed-skills.ts`
- Existing decision persistence: `apps/web/lib/decision-perspective/persistence.ts`
- Existing Build Studio decision gate: `apps/web/lib/decision-perspective/build-studio-gate.ts`
- Existing external evidence writer: `apps/web/lib/actions/external-evidence.ts`
- Existing compose safety wrapper: `scripts/dpf-compose.mjs` and `scripts/lib/compose-safety.mjs`

WWMD decisions already made:

- Environment process: `two-shared-nonprod-plus-local-ci` over per-thread servers or remote-CI-only, high confidence.
- Lease storage: `minimal-lease-model` over `ExternalEvidenceRecord`-only, high confidence.

## Live Planning State

Live MCP backlog check on 2026-05-26 confirmed this plan is an integration slice across existing epics, not a new standalone epic:

- `EP-BUILD-STUDIO` is the primary delivery epic. It already contains active reliability and UX-adjacent work such as task-scoped execution context, progress visibility, reset/retry recovery, sandbox baseline refresh, overlap sweeps, and wrong-tool dispatch fixes.
- `EP-BUILD-STUDIO-UX` owns the broader workflow-primary layout redesign; this plan must not fight that stream. Task 8 should add a small recommendation/evidence projection that can survive the owning layout redesign.
- `EP-WWMD-MCP` owns the MCP exposure for WWMD read/write tools. Decision-service work in this plan should use the governed MCP contract and should not backdoor DB writes if token scope is insufficient.
- `EP-BUILD-65837F` owns formal deliberation and constructive-conflict review patterns. Founder Review Queue work should consume that direction instead of creating another review vocabulary.
- `EP-REDUCTION-GEAR-ARCH` owns the DPF skill-pack formalization substrate. New skills must extend `packages/dpf-skill-pack`; do not create a parallel skill registry.

No backlog item should be filed from this plan without first checking overlap in those epics. If a task exposes a new implementation gap, attach it to the owning epic above rather than creating a new strategy bucket.

## File Structure

### Skill Pack

- Create `packages/dpf-skill-pack/skills/dpf-retrieve-decision-context/SKILL.md`
- Create `packages/dpf-skill-pack/skills/dpf-compare-options/SKILL.md`
- Create `packages/dpf-skill-pack/skills/dpf-record-decision-outcome/SKILL.md`
- Create `packages/dpf-skill-pack/skills/dpf-capture-kernel-gap/SKILL.md`
- Create `packages/dpf-skill-pack/skills/dpf-external-evidence-handoff/SKILL.md`
- Create `packages/dpf-skill-pack/skills/dpf-use-shared-nonprod-environment/SKILL.md`
- Create `packages/dpf-skill-pack/skills/dpf-local-merge-ci-before-push/SKILL.md`
- Create `packages/dpf-skill-pack/capability-packs.json`
- Modify `packages/dpf-skill-pack/README.md`
- Modify `packages/db/src/seed-skills.test.ts`

### Build Studio Services

- Create `apps/web/lib/build/decision-service.ts`
- Create `apps/web/lib/build/decision-service.test.ts`
- Create `apps/web/lib/build/capability-packs.ts`
- Create `apps/web/lib/build/capability-packs.test.ts`
- Modify `apps/web/lib/actions/build.ts` after the service is tested
- Modify `apps/web/components/build/BuildStudioWorkflowActionCard.tsx` after service integration is ready
- Modify `apps/web/components/build/BuildStudioWorkflowActionCard.test.tsx`

### External Evidence Intake

- Modify `packages/db/prisma/schema.prisma`
- Add migration generated by `pnpm --filter @dpf/db exec prisma migrate dev --create-only --name external_evidence_build_links`
- Modify `apps/web/lib/actions/external-evidence.ts`
- Modify `apps/web/lib/actions/external-evidence.test.ts`
- Modify `apps/web/lib/mcp-tools.ts`
- Create `apps/web/lib/mcp-tools-external-development-evidence.test.ts`
- Modify `apps/web/lib/skills/evidence-search.ts`

### Non-Production Environment Broker

- Modify `packages/db/prisma/schema.prisma`
- Add migration generated by `pnpm --filter @dpf/db exec prisma migrate dev --create-only --name nonprod_environment_leases`
- Create `apps/web/lib/nonprod/environment-lease.ts`
- Create `apps/web/lib/nonprod/environment-lease.test.ts`
- Create `apps/web/lib/nonprod/local-integration.ts`
- Create `apps/web/lib/nonprod/local-integration.test.ts`
- Create `scripts/local-integration-ci.mjs`
- Create `scripts/lib/local-integration-ci.mjs`
- Create `scripts/lib/local-integration-ci.test.mjs`
- Modify `apps/web/lib/mcp-tools.ts`
- Create `apps/web/lib/mcp-tools-nonprod-environments.test.ts`

### Founder Review Queue and UX

- Create `apps/web/lib/founder-review/queue.ts`
- Create `apps/web/lib/founder-review/queue.test.ts`
- Create `apps/web/app/(shell)/platform/ai/founder-review/page.tsx`
- Create `apps/web/app/(shell)/platform/ai/founder-review/page.test.tsx`
- Create `apps/web/components/build/EnvironmentStatusSummary.tsx`
- Create `apps/web/components/build/EnvironmentStatusSummary.test.tsx`
- Create `apps/web/components/build/UnifiedEvidenceTimeline.tsx`
- Create `apps/web/components/build/UnifiedEvidenceTimeline.test.tsx`
- Modify `apps/web/components/build/WorkflowStageInspector.tsx`
- Modify `apps/web/components/build/WorkflowStageInspector.test.tsx`
- Modify `apps/web/lib/explore/feature-build-types.ts`
- Modify `apps/web/lib/explore/feature-build-data.ts`
- Modify `apps/web/lib/explore/feature-build-data.test.ts`

## Execution Rules

- One PR-sized slice at a time.
- Each implementation session starts in its own worktree from current `origin/main`.
- Before pushing code, run local integration CI for the candidate branch against current `origin/main`.
- Use explicit `git add` commands that list every touched file for each commit.
- Do not start new app servers directly from a thread. Use the broker once it exists; until then, attach to the existing `localhost` environment unless a task explicitly says the broker is being built.
- Do not open a PR until the branch is ready to merge under AGENTS.md.
- Preserve route-to-route handoff context. The human should never have to copy a Build Studio id, TaskRun id, branch name, or evidence summary from one surface to another.
- Keep raw MCP names, skill ids, lease ids, and trace internals out of the default Build Studio and Founder Review views. They belong behind "View audit" or equivalent drill-down.
- Spend roughly 20 percent of each implementation slice removing duplicated decision/evidence projection logic, raw trace leakage, or UI-owned branching discovered by that slice.

## User Delight And Trust Gates

This plan has less visual surface area than a portal IA redesign, but the users still need to feel the system is helping rather than making them supervise machinery. Every implementation task must preserve the following user outcomes:

| User | Capability and challenge | Delight condition | Default view must hide | Verification smoke |
| ---- | ------------------------ | ----------------- | ---------------------- | ------------------ |
| Founder/operator | Owns final judgment but has limited attention; needs fast confidence and a recovery path | One card says what needs a decision, why, and the safest next action | MCP payloads, skill ids, raw trace dumps, duplicate evidence rows | Founder Review Queue shows grouped decisions with one primary action and links back to build/task context |
| Build Studio operator | Wants to move a build forward without understanding every tool call | Phase surface shows current state, recommendation, environment readiness, confidence/evidence summary, and one primary action | Null/unknown noise, raw skill routing, hidden source disagreement | `/build` shows a single action spine; audit details are reachable but not competing with the primary action |
| External Claude/Codex contributor | Works outside Build Studio but needs governed handoff | Evidence handoff records branch, files, tests, unresolved questions, and next action without manual copy-paste | Build Studio-only jargon that hides what the contributor must supply | External evidence intake accepts a complete handoff payload and timeline labels the source clearly |
| Reviewer/shipper | Needs to know whether work is merge-ready | Local integration gate produces a clear pass/fail/conflict result and blocks push on red | "Passed in my worktree" as final readiness | Local integration evidence is linked from the Build Studio timeline and PR readiness checklist |
| Platform maintainer | Needs reusable architecture, not one more page-specific branch | Decision, evidence, environment, and timeline projections are typed services used by UI | UI components that directly encode WWMD, lease, or evidence branching | Service tests cover the projection, and Task 8 only renders already-normalized data |

Default language examples:

- Prefer: "Recommended next action: use the shared environment."
- Avoid in default UI: "Capability pack selected Verification and invoked `dpf-use-shared-nonprod-environment`."
- Prefer: "Needs founder review: evidence gap."
- Avoid in default UI: "DecisionInteraction outcomeType=defer, outcomePayload.unresolvedReason=evidence-gap."
- Prefer: "Merged-code gate failed: conflict with `origin/main`."
- Avoid in default UI: "local-integration/local-integration-feature branch returned status 1."

## Task 1: Skill Pack Extension

**Files:**
- Create: `packages/dpf-skill-pack/skills/dpf-retrieve-decision-context/SKILL.md`
- Create: `packages/dpf-skill-pack/skills/dpf-compare-options/SKILL.md`
- Create: `packages/dpf-skill-pack/skills/dpf-record-decision-outcome/SKILL.md`
- Create: `packages/dpf-skill-pack/skills/dpf-capture-kernel-gap/SKILL.md`
- Create: `packages/dpf-skill-pack/skills/dpf-external-evidence-handoff/SKILL.md`
- Create: `packages/dpf-skill-pack/skills/dpf-use-shared-nonprod-environment/SKILL.md`
- Create: `packages/dpf-skill-pack/skills/dpf-local-merge-ci-before-push/SKILL.md`
- Modify: `packages/dpf-skill-pack/README.md`
- Test: `packages/db/src/seed-skills.test.ts`

- [ ] **Step 1: Add a seed invariant test for the new skill names**

Add this test inside the existing `describe("dpf-platform mirror-field invariant", ...)` block in `packages/db/src/seed-skills.test.ts`:

```ts
it("includes the Build Studio decision and nonprod workflow skills", () => {
  const pluginSkillsDir = join(__dirname, "..", "..", "..", "packages", "dpf-skill-pack", "skills");
  const names = discoverDpfPlatformSkillFiles(pluginSkillsDir)
    .map((source) => parseFrontmatter(readFileSync(source.filePath, "utf8")).frontmatter.name)
    .sort();

  expect(names).toEqual(expect.arrayContaining([
    "dpf-capture-kernel-gap",
    "dpf-compare-options",
    "dpf-external-evidence-handoff",
    "dpf-local-merge-ci-before-push",
    "dpf-record-decision-outcome",
    "dpf-retrieve-decision-context",
    "dpf-use-shared-nonprod-environment",
  ]));
});
```

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

Run:

```powershell
pnpm --filter @dpf/db exec vitest run src/seed-skills.test.ts
```

Expected: FAIL because the seven new skill directories do not exist.

- [ ] **Step 3: Create the new skill files**

Create each `SKILL.md` with the PR #1168 superset frontmatter. Use this exact frontmatter pattern and adjust only `name`, `description`, `category`, `taskType`, `triggerPattern`, `allowed-tools`, `allowedTools`, `composesFrom`, and `enforces` per the table below.

```md
---
name: dpf-retrieve-decision-context
description: "Use in the DPF codebase before a WWMD/kernel decision when the agent needs repo, backlog, spec, evidence, or principle context. Queries DPF MCP and local repo state before options are scored."
disable-model-invocation: false
user-invocable: true
allowed-tools: mcp__dpf__wiki_query mcp__dpf__search_specs_and_plans mcp__dpf__list_backlog_items mcp__dpf__list_epics Bash(rg *)
category: governance
assignTo: ["*"]
capability: null
taskType: deliberation
triggerPattern: "decision context|retrieve context|before WWMD|kernel context|principle context"
userInvocable: true
agentInvocable: true
allowedTools: ["mcp__dpf__wiki_query", "mcp__dpf__search_specs_and_plans", "mcp__dpf__list_backlog_items", "mcp__dpf__list_epics", "Bash"]
composesFrom: ["dpf-verify-substrate-first"]
contextRequirements: ["DPF MCP read tools reachable; repo available for rg"]
riskBand: low
enforces:
  - kernel/principles/verify-substrate-before-proposing-new
  - kernel/principles/live-state-over-seed-data
  - kernel/principles/no-assumptions
---

# DPF Retrieve Decision Context

Gather the evidence needed before a WWMD decision. Query current specs, live backlog, and relevant principles before scoring options.
```

Use this table for the remaining files:

| File | name | allowed-tools | category | taskType | composesFrom | enforces |
|---|---|---|---|---|---|---|
| `dpf-compare-options/SKILL.md` | `dpf-compare-options` | `mcp__dpf__principle_decide mcp__dpf__wiki_query` | `governance` | `deliberation` | `["dpf-retrieve-decision-context"]` | `architecture-over-shortcuts`, `research-before-implementing` |
| `dpf-record-decision-outcome/SKILL.md` | `dpf-record-decision-outcome` | `mcp__dpf__save_build_notes mcp__dpf__record_execution_evidence` | `governance` | `evidence` | `["dpf-decision-via-kernel"]` | `evidence-before-diagnosis`, `single-source-of-truth` |
| `dpf-capture-kernel-gap/SKILL.md` | `dpf-capture-kernel-gap` | `mcp__dpf__save_build_notes mcp__dpf__create_backlog_item` | `governance` | `deliberation` | `["dpf-decision-via-kernel"]` | `architecture-over-shortcuts`, `single-source-of-truth` |
| `dpf-external-evidence-handoff/SKILL.md` | `dpf-external-evidence-handoff` | `mcp__dpf__record_execution_evidence mcp__dpf__save_build_notes` | `build` | `evidence` | `["dpf-evidence-before-diagnosis"]` | `never-fabricate`, `build-gate-mandatory` |
| `dpf-use-shared-nonprod-environment/SKILL.md` | `dpf-use-shared-nonprod-environment` | `mcp__dpf__list_nonprod_environment_leases mcp__dpf__claim_nonprod_environment_lease mcp__dpf__release_nonprod_environment_lease` | `ops` | `workflow` | `["dpf-worktree-per-session"]` | `no-assumptions`, `responsible-capacity-utilization` |
| `dpf-local-merge-ci-before-push/SKILL.md` | `dpf-local-merge-ci-before-push` | `Bash(git *) Bash(pnpm *) Bash(node scripts/local-integration-ci.mjs *) mcp__dpf__record_local_integration_result` | `build` | `workflow` | `["dpf-pr-with-dco"]` | `build-gate-mandatory`, `all-changes-land-via-pr` |

Each body must include a "When to use", "Steps", "Guardrails", and "Worked example" section. Keep every body under 500 lines.

- [ ] **Step 4: Update the README skill table**

Add the seven new rows under `## Skills shipped in v0.1.0` in `packages/dpf-skill-pack/README.md`. Use the same four-column table format already present.

- [ ] **Step 5: Run the seed skill tests**

Run:

```powershell
pnpm --filter @dpf/db exec vitest run src/seed-skills.test.ts
```

Expected: PASS. The new invariant and existing mirror-field invariant pass.

- [ ] **Step 6: Commit**

Run:

```powershell
git add packages/dpf-skill-pack/skills/dpf-retrieve-decision-context/SKILL.md packages/dpf-skill-pack/skills/dpf-compare-options/SKILL.md packages/dpf-skill-pack/skills/dpf-record-decision-outcome/SKILL.md packages/dpf-skill-pack/skills/dpf-capture-kernel-gap/SKILL.md packages/dpf-skill-pack/skills/dpf-external-evidence-handoff/SKILL.md packages/dpf-skill-pack/skills/dpf-use-shared-nonprod-environment/SKILL.md packages/dpf-skill-pack/skills/dpf-local-merge-ci-before-push/SKILL.md packages/dpf-skill-pack/README.md packages/db/src/seed-skills.test.ts
git commit -s -m "feat: extend DPF decision skill pack"
```

## Task 2: Capability Pack Metadata

**Files:**
- Create: `packages/dpf-skill-pack/capability-packs.json`
- Create: `apps/web/lib/build/capability-packs.ts`
- Create: `apps/web/lib/build/capability-packs.test.ts`
- Modify: `packages/dpf-skill-pack/README.md`

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

Create `apps/web/lib/build/capability-packs.test.ts`:

```ts
import { describe, expect, it } from "vitest";
import { getBuildStudioCapabilityPack, listBuildStudioCapabilityPacks } from "./capability-packs";

describe("Build Studio capability packs", () => {
  it("loads the six core packs and keeps skill ids stable", () => {
    const packs = listBuildStudioCapabilityPacks();
    expect(packs.map((pack) => pack.id)).toEqual([
      "architecture",
      "design",
      "implementation",
      "verification",
      "review-ship",
      "recovery",
    ]);
    expect(getBuildStudioCapabilityPack("review-ship").skillIds).toContain("dpf-local-merge-ci-before-push");
    expect(getBuildStudioCapabilityPack("implementation").skillIds).toContain("dpf-use-shared-nonprod-environment");
  });
});
```

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

Run:

```powershell
pnpm --filter web exec vitest run lib/build/capability-packs.test.ts
```

Expected: FAIL because `apps/web/lib/build/capability-packs.ts` does not exist.

- [ ] **Step 3: Create pack metadata**

Create `packages/dpf-skill-pack/capability-packs.json`:

```json
[
  {
    "id": "architecture",
    "label": "Architecture",
    "skillIds": ["dpf-retrieve-decision-context", "dpf-compare-options", "dpf-decision-via-kernel"]
  },
  {
    "id": "design",
    "label": "Design",
    "skillIds": ["dpf-retrieve-decision-context", "dpf-compare-options", "dpf-capture-kernel-gap"]
  },
  {
    "id": "implementation",
    "label": "Implementation",
    "skillIds": ["dpf-worktree-per-session", "dpf-use-shared-nonprod-environment", "dpf-evidence-before-diagnosis"]
  },
  {
    "id": "verification",
    "label": "Verification",
    "skillIds": ["dpf-evidence-before-diagnosis", "dpf-external-evidence-handoff", "dpf-record-decision-outcome"]
  },
  {
    "id": "review-ship",
    "label": "Review/Ship",
    "skillIds": ["dpf-pr-with-dco", "dpf-local-merge-ci-before-push", "dpf-record-decision-outcome"]
  },
  {
    "id": "recovery",
    "label": "Recovery",
    "skillIds": ["dpf-evidence-before-diagnosis", "dpf-capture-kernel-gap", "dpf-use-shared-nonprod-environment"]
  }
]
```

- [ ] **Step 4: Create the loader**

Create `apps/web/lib/build/capability-packs.ts`:

```ts
import packs from "../../../../packages/dpf-skill-pack/capability-packs.json";

export type BuildStudioCapabilityPackId =
  | "architecture"
  | "design"
  | "implementation"
  | "verification"
  | "review-ship"
  | "recovery";

export type BuildStudioCapabilityPack = {
  id: BuildStudioCapabilityPackId;
  label: string;
  skillIds: string[];
};

const ORDER: BuildStudioCapabilityPackId[] = [
  "architecture",
  "design",
  "implementation",
  "verification",
  "review-ship",
  "recovery",
];

const PACKS = packs as BuildStudioCapabilityPack[];

export function listBuildStudioCapabilityPacks(): BuildStudioCapabilityPack[] {
  return ORDER.map((id) => getBuildStudioCapabilityPack(id));
}

export function getBuildStudioCapabilityPack(id: BuildStudioCapabilityPackId): BuildStudioCapabilityPack {
  const pack = PACKS.find((candidate) => candidate.id === id);
  if (!pack) {
    throw new Error(`Missing Build Studio capability pack: ${id}`);
  }
  return pack;
}
```

- [ ] **Step 5: Run the test**

Run:

```powershell
pnpm --filter web exec vitest run lib/build/capability-packs.test.ts
```

Expected: PASS.

- [ ] **Step 6: Commit**

Run:

```powershell
git add packages/dpf-skill-pack/capability-packs.json packages/dpf-skill-pack/README.md apps/web/lib/build/capability-packs.ts apps/web/lib/build/capability-packs.test.ts
git commit -s -m "feat: add Build Studio capability pack metadata"
```

## Task 3: Build Studio Decision Service

**Files:**
- Create: `apps/web/lib/build/decision-service.ts`
- Create: `apps/web/lib/build/decision-service.test.ts`
- Modify: `apps/web/lib/actions/build.ts`
- Modify: `apps/web/lib/actions/build-governed.test.ts`

- [ ] **Step 1: Write the failing service tests**

Create `apps/web/lib/build/decision-service.test.ts`:

```ts
import { describe, expect, it, vi } from "vitest";
import { evaluateBuildStudioDecision } from "./decision-service";

describe("evaluateBuildStudioDecision", () => {
  it("returns a recommendation when principle_decide has high confidence", async () => {
    const toolExecutor = vi.fn().mockResolvedValue({
      success: true,
      data: {
        recommendation: { optionId: "shared-env", confidence: "high", margin: 1.2 },
        flags: { commandmentConflict: false },
        reasoning: "Use shared environment.",
      },
    });
    const result = await evaluateBuildStudioDecision({
      toolExecutor,
      userId: "user-1",
      request: {
        source: "build-studio",
        routeContext: "/build",
        buildId: "FB-1",
        phase: "build",
        question: "Which verification environment should this branch use?",
        options: [
          { id: "shared-env", description: "Use Nonprod A.", operatorLabel: "Use shared environment" },
          { id: "thread-server", description: "Start a per-thread server." },
        ],
      },
    });
    expect(result.status).toBe("recommended");
    expect(result.recommendation?.optionId).toBe("shared-env");
    expect(result.operatorActionLabel).toBe("Use shared environment");
    expect(result.reasonSummary).not.toMatch(/mcp|principle_decide|dpf-/i);
  });

  it("blocks when the kernel reports a commandment conflict", async () => {
    const toolExecutor = vi.fn().mockResolvedValue({
      success: true,
      data: {
        recommendation: { optionId: "thread-server", confidence: "high", margin: 0.9 },
        flags: { commandmentConflict: true },
        reasoning: "A commandment blocks this action.",
      },
    });
    const result = await evaluateBuildStudioDecision({
      toolExecutor,
      userId: "user-1",
      request: {
        source: "build-studio",
        routeContext: "/build",
        buildId: "FB-1",
        phase: "build",
        question: "Start a new unmanaged server?",
        options: [
          { id: "shared-env", description: "Use Nonprod A.", operatorLabel: "Use shared environment" },
          { id: "thread-server", description: "Start a per-thread server." },
        ],
      },
    });
    expect(result.status).toBe("blocked");
    expect(result.operatorActionLabel).toBe("Resolve blocker before continuing");
  });
});
```

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

Run:

```powershell
pnpm --filter web exec vitest run lib/build/decision-service.test.ts
```

Expected: FAIL because `decision-service.ts` does not exist.

- [ ] **Step 3: Create the service**

Create `apps/web/lib/build/decision-service.ts`:

```ts
import { executeTool } from "@/lib/mcp-tools";

export type BuildStudioDecisionSource = "build-studio" | "claude" | "codex" | "coworker";
export type BuildStudioDecisionStatus = "recommended" | "needs-human" | "captured-gap" | "blocked";

export type BuildStudioDecisionOption = {
  id: string;
  description: string;
  operatorLabel?: string;
  features?: Record<string, number>;
};

export type BuildStudioDecisionRequest = {
  source: BuildStudioDecisionSource;
  routeContext: string;
  buildId?: string;
  taskRunId?: string;
  phase?: string;
  question: string;
  options: BuildStudioDecisionOption[];
};

export type BuildStudioDecisionResult = {
  status: BuildStudioDecisionStatus;
  recommendation?: { optionId: string; confidence: string; margin: number };
  reasonSummary: string;
  operatorActionLabel: string;
  auditSummary: string;
};

export type BuildStudioDecisionToolExecutor = (
  toolName: string,
  args: Record<string, unknown>,
  userId: string,
  context: { routeContext: string },
) => Promise<{ success?: boolean; data?: any; error?: string }>;

function labelFromOption(option: BuildStudioDecisionOption | undefined, fallback: string) {
  if (!option) return fallback;
  return option.operatorLabel ?? option.description.replace(/\.$/, "");
}

function sanitizeOperatorSummary(value: unknown, fallback: string) {
  const text = typeof value === "string" && value.trim().length > 0 ? value : fallback;
  return text
    .replace(/\bprinciple_decide\b/gi, "the decision service")
    .replace(/\bmcp\b/gi, "governed tools")
    .replace(/\bdpf-[a-z0-9-]+\b/gi, "the matching DPF skill");
}

export async function evaluateBuildStudioDecision(input: {
  request: BuildStudioDecisionRequest;
  userId: string;
  toolExecutor?: BuildStudioDecisionToolExecutor;
}): Promise<BuildStudioDecisionResult> {
  const toolExecutor = input.toolExecutor ?? executeTool;
  const response = await toolExecutor(
    "principle_decide",
    {
      context: input.request.question,
      callingPopulation: input.request.source === "build-studio" ? "in_platform_coworker" : "external_coding_agent",
      options: input.request.options,
    },
    input.userId,
    { routeContext: input.request.routeContext },
  );

  if (!response.success) {
    return {
      status: "captured-gap",
      reasonSummary: sanitizeOperatorSummary(response.error, "WWMD did not return a decision."),
      operatorActionLabel: "Send to founder review",
      auditSummary: "Decision service could not obtain a governed recommendation.",
    };
  }

  const flags = response.data?.flags ?? {};
  const recommendation = response.data?.recommendation;
  if (flags.commandmentConflict === true) {
    return {
      status: "blocked",
      recommendation,
      reasonSummary: sanitizeOperatorSummary(response.data?.reasoning, "A commandment blocks this decision."),
      operatorActionLabel: "Resolve blocker before continuing",
      auditSummary: `Blocked by governed decision response for ${input.request.question}.`,
    };
  }

  if (!recommendation || recommendation.confidence !== "high") {
    return {
      status: "needs-human",
      recommendation,
      reasonSummary: sanitizeOperatorSummary(response.data?.reasoning, "WWMD requires human review."),
      operatorActionLabel: "Ask for human decision",
      auditSummary: `Decision requires human review for ${input.request.question}.`,
    };
  }

  const recommendedOption = input.request.options.find((option) => option.id === recommendation.optionId);
  return {
    status: "recommended",
    recommendation,
    reasonSummary: sanitizeOperatorSummary(response.data?.reasoning, `WWMD recommends ${recommendation.optionId}.`),
    operatorActionLabel: labelFromOption(recommendedOption, "Use recommended option"),
    auditSummary: `Governed decision recommended ${recommendation.optionId} with ${recommendation.confidence} confidence.`,
  };
}
```

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

Run:

```powershell
pnpm --filter web exec vitest run lib/build/decision-service.test.ts
```

Expected: PASS.

- [ ] **Step 5: Wire only the plan-to-build decision through the service**

Modify `apps/web/lib/actions/build.ts` near the existing `evaluateBuildStudioPlanAdvancementGate` call. Keep the current gate as the persisted ledger writer. Add the new decision service as a pre-gate recommendation summary only, so this slice is low risk.

Acceptance in `apps/web/lib/actions/build-governed.test.ts`:

```ts
expect(mockEvaluateBuildStudioPlanAdvancementGate).toHaveBeenCalled();
expect(mockEvaluateBuildStudioDecision).toHaveBeenCalledWith(expect.objectContaining({
  request: expect.objectContaining({
    source: "build-studio",
    routeContext: "/build",
  }),
}));
```

- [ ] **Step 6: Run affected tests**

Run:

```powershell
pnpm --filter web exec vitest run lib/build/decision-service.test.ts lib/actions/build-governed.test.ts
```

Expected: PASS.

- [ ] **Step 7: Commit**

Run:

```powershell
git add apps/web/lib/build/decision-service.ts apps/web/lib/build/decision-service.test.ts apps/web/lib/actions/build.ts apps/web/lib/actions/build-governed.test.ts
git commit -s -m "feat: add Build Studio decision service"
```

## Task 4: External Development Evidence Intake

**Files:**
- Modify: `packages/db/prisma/schema.prisma`
- Create via command: `pnpm --filter @dpf/db exec prisma migrate dev --create-only --name external_evidence_build_links`
- Modify: `apps/web/lib/actions/external-evidence.ts`
- Modify: `apps/web/lib/actions/external-evidence.test.ts`
- Modify: `apps/web/lib/mcp-tools.ts`
- Create: `apps/web/lib/mcp-tools-external-development-evidence.test.ts`

- [ ] **Step 1: Add failing external evidence link test**

Update `apps/web/lib/actions/external-evidence.test.ts` with:

```ts
it("writes optional build and task links for external development evidence", async () => {
  mockPrisma.externalEvidenceRecord.create.mockResolvedValue({ id: "evidence-2" });

  await recordExternalEvidence({
    actorUserId: "user-1",
    routeContext: "/build",
    operationType: "external_development_handoff",
    target: "codex-session-1",
    provider: "codex",
    resultSummary: "Local integration passed.",
    buildId: "FB-123",
    taskRunId: "TR-123",
    details: { commits: ["abc123"] },
  });

  expect(mockPrisma.externalEvidenceRecord.create).toHaveBeenCalledWith({
    data: expect.objectContaining({
      buildId: "FB-123",
      taskRunId: "TR-123",
    }),
  });
});
```

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

Run:

```powershell
pnpm --filter web exec vitest run lib/actions/external-evidence.test.ts
```

Expected: FAIL because `recordExternalEvidence` does not accept `buildId` or `taskRunId`.

- [ ] **Step 3: Add schema fields**

Modify `ExternalEvidenceRecord` in `packages/db/prisma/schema.prisma`:

```prisma
model ExternalEvidenceRecord {
  id            String   @id @default(cuid())
  actorUserId   String
  routeContext  String
  operationType String
  target        String
  provider      String
  resultSummary String
  details       Json?
  buildId       String?
  taskRunId     String?
  createdAt     DateTime @default(now())
  actorUser     User     @relation(fields: [actorUserId], references: [id], onDelete: Cascade)

  @@index([actorUserId, createdAt])
  @@index([routeContext, operationType])
  @@index([operationType, createdAt])
  @@index([buildId, createdAt])
  @@index([taskRunId, createdAt])
}
```

Generate migration:

```powershell
pnpm --filter @dpf/db exec prisma migrate dev --create-only --name external_evidence_build_links
```

Expected SQL:

```sql
ALTER TABLE "ExternalEvidenceRecord" ADD COLUMN "buildId" TEXT;
ALTER TABLE "ExternalEvidenceRecord" ADD COLUMN "taskRunId" TEXT;
CREATE INDEX "ExternalEvidenceRecord_buildId_createdAt_idx" ON "ExternalEvidenceRecord"("buildId", "createdAt");
CREATE INDEX "ExternalEvidenceRecord_taskRunId_createdAt_idx" ON "ExternalEvidenceRecord"("taskRunId", "createdAt");
```

- [ ] **Step 4: Update the writer**

Modify `apps/web/lib/actions/external-evidence.ts` input type and create call:

```ts
export async function recordExternalEvidence(input: {
  actorUserId: string;
  routeContext: string;
  operationType: string;
  target: string;
  provider: string;
  resultSummary: string;
  buildId?: string;
  taskRunId?: string;
  details?: Prisma.InputJsonValue;
}) {
  return prisma.externalEvidenceRecord.create({
    data: {
      actorUserId: input.actorUserId,
      routeContext: input.routeContext,
      operationType: input.operationType,
      target: input.target,
      provider: input.provider,
      resultSummary: input.resultSummary,
      ...(input.buildId !== undefined ? { buildId: input.buildId } : {}),
      ...(input.taskRunId !== undefined ? { taskRunId: input.taskRunId } : {}),
      ...(input.details !== undefined ? { details: input.details } : {}),
    },
  });
}
```

- [ ] **Step 5: Add MCP tool test**

Create `apps/web/lib/mcp-tools-external-development-evidence.test.ts`:

```ts
import { describe, expect, it, vi } from "vitest";
import { executeTool } from "./mcp-tools";

vi.mock("@/lib/actions/external-evidence", () => ({
  recordExternalEvidence: vi.fn().mockResolvedValue({ id: "external-1" }),
}));

describe("record_external_development_evidence", () => {
  it("records external handoff evidence with build and integration details", async () => {
    const result = await executeTool("record_external_development_evidence", {
      provider: "codex",
      externalSessionId: "codex-1",
      buildId: "FB-1",
      taskRunId: "TR-1",
      routeContext: "/build",
      summary: "Merged-code gate passed.",
      commits: ["abc123"],
      localIntegration: { status: "passed", mode: "single-branch" },
    }, "user-1", { routeContext: "/build" });

    expect(result.success).toBe(true);
  });
});
```

- [ ] **Step 6: Register the MCP tool**

Modify `apps/web/lib/mcp-tools.ts`:

- Add a `PLATFORM_TOOLS` entry named `record_external_development_evidence`.
- Add an `executeTool` case that validates `provider`, `externalSessionId`, `summary`, and `routeContext`.
- Call `recordExternalEvidence` with `operationType: "external_development_handoff"`.
- Store commits, changed files, verification, local integration, unresolved questions, and skill IDs in `details`.

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

Run:

```powershell
pnpm --filter web exec vitest run lib/actions/external-evidence.test.ts lib/mcp-tools-external-development-evidence.test.ts
pnpm --filter @dpf/db exec prisma validate
```

Expected: PASS.

- [ ] **Step 8: Commit**

Run:

```powershell
git add packages/db/prisma/schema.prisma packages/db/prisma/migrations apps/web/lib/actions/external-evidence.ts apps/web/lib/actions/external-evidence.test.ts apps/web/lib/mcp-tools.ts apps/web/lib/mcp-tools-external-development-evidence.test.ts
git commit -s -m "feat: record external development evidence"
```

## Task 5: Non-Production Environment Lease Model and Broker

**Files:**
- Modify: `packages/db/prisma/schema.prisma`
- Create via command: `pnpm --filter @dpf/db exec prisma migrate dev --create-only --name nonprod_environment_leases`
- Create: `apps/web/lib/nonprod/environment-lease.ts`
- Create: `apps/web/lib/nonprod/environment-lease.test.ts`
- Modify: `apps/web/lib/mcp-tools.ts`
- Create: `apps/web/lib/mcp-tools-nonprod-environments.test.ts`

- [ ] **Step 1: Write the lease service tests**

Create `apps/web/lib/nonprod/environment-lease.test.ts`:

```ts
import { describe, expect, it, vi } from "vitest";
import { claimNonprodEnvironmentLease, listActiveNonprodEnvironmentLeases, releaseNonprodEnvironmentLease } from "./environment-lease";

function db() {
  return {
    nonProductionEnvironmentLease: {
      findMany: vi.fn().mockResolvedValue([]),
      findFirst: vi.fn().mockResolvedValue(null),
      create: vi.fn().mockResolvedValue({ leaseId: "NPEL-1" }),
      update: vi.fn().mockResolvedValue({ leaseId: "NPEL-1", status: "released" }),
    },
  };
}

describe("non-production environment leases", () => {
  it("claims an available environment", async () => {
    const mockDb = db();
    const result = await claimNonprodEnvironmentLease({
      db: mockDb as any,
      environmentKey: "active-candidate",
      ownerProvider: "codex",
      ownerSessionId: "session-1",
      purpose: "UX verification",
      url: "http://localhost:53601",
      ports: [53601],
      expiresAt: new Date("2026-05-26T18:00:00.000Z"),
    });
    expect(result.status).toBe("claimed");
    expect(mockDb.nonProductionEnvironmentLease.create).toHaveBeenCalled();
  });

  it("refuses a conflicting active lease", async () => {
    const mockDb = db();
    mockDb.nonProductionEnvironmentLease.findFirst.mockResolvedValue({ leaseId: "NPEL-ACTIVE" });
    const result = await claimNonprodEnvironmentLease({
      db: mockDb as any,
      environmentKey: "active-candidate",
      ownerProvider: "claude",
      ownerSessionId: "session-2",
      purpose: "Second server",
      url: "http://localhost:53602",
      ports: [53602],
      expiresAt: new Date("2026-05-26T18:00:00.000Z"),
    });
    expect(result.status).toBe("conflict");
  });
});
```

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

Run:

```powershell
pnpm --filter web exec vitest run lib/nonprod/environment-lease.test.ts
```

Expected: FAIL because the lease service does not exist.

- [ ] **Step 3: Add schema model**

Add this model to `packages/db/prisma/schema.prisma` near `ExternalEvidenceRecord`:

```prisma
model NonProductionEnvironmentLease {
  id              String    @id @default(cuid())
  leaseId         String    @unique
  environmentKey  String
  status          String    @default("active")
  ownerProvider   String
  ownerSessionId  String
  purpose         String
  worktreePath    String?
  branchName      String?
  buildId         String?
  taskRunId       String?
  url             String
  ports           Int[]     @default([])
  cleanupCommand  String?
  evidenceRecordId String?
  expiresAt       DateTime
  releasedAt      DateTime?
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  @@index([environmentKey, status])
  @@index([ownerProvider, ownerSessionId])
  @@index([buildId, createdAt])
  @@index([taskRunId, createdAt])
  @@index([expiresAt])
}
```

Generate migration:

```powershell
pnpm --filter @dpf/db exec prisma migrate dev --create-only --name nonprod_environment_leases
```

- [ ] **Step 4: Create lease service**

Create `apps/web/lib/nonprod/environment-lease.ts`:

```ts
import { randomUUID } from "node:crypto";
import { prisma } from "@dpf/db";

export type NonprodEnvironmentKey = "active-candidate" | "local-integration-ci";
export type NonprodOwnerProvider = "build-studio" | "claude" | "codex" | "coworker";

type LeaseDb = Pick<typeof prisma, "nonProductionEnvironmentLease">;

export async function listActiveNonprodEnvironmentLeases(input: {
  db?: LeaseDb;
  now?: Date;
}) {
  const db = input.db ?? prisma;
  const now = input.now ?? new Date();
  return db.nonProductionEnvironmentLease.findMany({
    where: {
      status: "active",
      expiresAt: { gt: now },
    },
    orderBy: { createdAt: "desc" },
  });
}

export async function claimNonprodEnvironmentLease(input: {
  db?: LeaseDb;
  environmentKey: NonprodEnvironmentKey;
  ownerProvider: NonprodOwnerProvider;
  ownerSessionId: string;
  purpose: string;
  url: string;
  ports: number[];
  expiresAt: Date;
  worktreePath?: string;
  branchName?: string;
  buildId?: string;
  taskRunId?: string;
  cleanupCommand?: string;
}) {
  const db = input.db ?? prisma;
  const active = await db.nonProductionEnvironmentLease.findFirst({
    where: {
      environmentKey: input.environmentKey,
      status: "active",
      expiresAt: { gt: new Date() },
    },
  });
  if (active) return { status: "conflict" as const, active };

  const lease = await db.nonProductionEnvironmentLease.create({
    data: {
      leaseId: `NPEL-${randomUUID().replace(/-/g, "").slice(0, 10).toUpperCase()}`,
      environmentKey: input.environmentKey,
      ownerProvider: input.ownerProvider,
      ownerSessionId: input.ownerSessionId,
      purpose: input.purpose,
      url: input.url,
      ports: input.ports,
      expiresAt: input.expiresAt,
      worktreePath: input.worktreePath,
      branchName: input.branchName,
      buildId: input.buildId,
      taskRunId: input.taskRunId,
      cleanupCommand: input.cleanupCommand,
    },
  });
  return { status: "claimed" as const, lease };
}

export async function releaseNonprodEnvironmentLease(input: {
  db?: LeaseDb;
  leaseId: string;
  now?: Date;
}) {
  const db = input.db ?? prisma;
  return db.nonProductionEnvironmentLease.update({
    where: { leaseId: input.leaseId },
    data: {
      status: "released",
      releasedAt: input.now ?? new Date(),
    },
  });
}
```

- [ ] **Step 5: Add MCP tools**

Modify `apps/web/lib/mcp-tools.ts` to add:

- `list_nonprod_environment_leases`
- `claim_nonprod_environment_lease`
- `release_nonprod_environment_lease`

Each tool calls the service and returns structured success/error JSON.

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

Run:

```powershell
pnpm --filter web exec vitest run lib/nonprod/environment-lease.test.ts lib/mcp-tools-nonprod-environments.test.ts
pnpm --filter @dpf/db exec prisma validate
```

Expected: PASS.

- [ ] **Step 7: Commit**

Run:

```powershell
git add packages/db/prisma/schema.prisma packages/db/prisma/migrations apps/web/lib/nonprod/environment-lease.ts apps/web/lib/nonprod/environment-lease.test.ts apps/web/lib/mcp-tools.ts apps/web/lib/mcp-tools-nonprod-environments.test.ts
git commit -s -m "feat: broker nonproduction environment leases"
```

## Task 6: Local Integration CI Runner

**Files:**
- Create: `scripts/lib/local-integration-ci.mjs`
- Create: `scripts/lib/local-integration-ci.test.mjs`
- Create: `scripts/local-integration-ci.mjs`
- Create: `apps/web/lib/nonprod/local-integration.ts`
- Create: `apps/web/lib/nonprod/local-integration.test.ts`
- Modify: `apps/web/lib/mcp-tools.ts`

- [ ] **Step 1: Write planner tests**

Create `scripts/lib/local-integration-ci.test.mjs`:

```js
import { describe, expect, it } from "vitest";
import { createLocalIntegrationPlan } from "./local-integration-ci.mjs";

describe("createLocalIntegrationPlan", () => {
  it("plans a single-branch local integration run", () => {
    const plan = createLocalIntegrationPlan({
      candidateBranch: "doc/build-studio-decision-skill-packs",
      mode: "single-branch",
      siblingBranches: [],
    });
    expect(plan.commands.map((command) => command.join(" "))).toEqual([
      "git fetch origin main",
      "git checkout -B local-integration/doc-build-studio-decision-skill-packs origin/main",
      "git merge --no-ff --no-edit doc/build-studio-decision-skill-packs",
      "pnpm --filter web exec vitest run",
      "pnpm --filter web typecheck",
      "pnpm --filter web exec next build",
    ]);
  });
});
```

- [ ] **Step 2: Run and verify failure**

Run:

```powershell
pnpm exec vitest run scripts/lib/local-integration-ci.test.mjs
```

Expected: FAIL because the planner module does not exist.

- [ ] **Step 3: Create planner module**

Create `scripts/lib/local-integration-ci.mjs`:

```js
export function integrationBranchName(candidateBranch) {
  return `local-integration/${candidateBranch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`;
}

export function createLocalIntegrationPlan(input) {
  const branch = integrationBranchName(input.candidateBranch);
  const commands = [
    ["git", "fetch", "origin", "main"],
    ["git", "checkout", "-B", branch, "origin/main"],
    ["git", "merge", "--no-ff", "--no-edit", input.candidateBranch],
    ...input.siblingBranches.map((sibling) => ["git", "merge", "--no-ff", "--no-edit", sibling]),
    ["pnpm", "--filter", "web", "exec", "vitest", "run"],
    ["pnpm", "--filter", "web", "typecheck"],
    ["pnpm", "--filter", "web", "exec", "next", "build"],
  ];
  return {
    mode: input.mode,
    integrationBranch: branch,
    commands,
  };
}
```

- [ ] **Step 4: Create CLI runner**

Create `scripts/local-integration-ci.mjs`:

```js
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createLocalIntegrationPlan } from "./lib/local-integration-ci.mjs";

function valueAfter(flag) {
  const index = process.argv.indexOf(flag);
  return index >= 0 ? process.argv[index + 1] : "";
}

const candidateBranch = valueAfter("--candidate");
const mode = valueAfter("--mode") || "single-branch";
const siblingBranches = process.argv
  .filter((arg) => arg.startsWith("--sibling="))
  .map((arg) => arg.slice("--sibling=".length));

if (!candidateBranch) {
  console.error("Usage: node scripts/local-integration-ci.mjs --candidate BRANCH [--mode single-branch|sibling-set|post-merge-main] [--sibling=BRANCH]");
  process.exit(2);
}

const plan = createLocalIntegrationPlan({ candidateBranch, mode, siblingBranches });
for (const command of plan.commands) {
  console.log(`[local-integration-ci] ${command.join(" ")}`);
  const result = spawnSync(command[0], command.slice(1), { stdio: "inherit", shell: process.platform === "win32" });
  if (result.status !== 0) process.exit(result.status ?? 1);
}
```

- [ ] **Step 5: Add web-side result recorder**

Create `apps/web/lib/nonprod/local-integration.ts` with:

```ts
import type { Prisma } from "@dpf/db";
import { recordExternalEvidence } from "@/lib/actions/external-evidence";

export type LocalIntegrationResultInput = {
  actorUserId: string;
  provider: "build-studio" | "claude" | "codex" | "coworker";
  externalSessionId: string;
  routeContext: string;
  buildId?: string;
  taskRunId?: string;
  candidateBranch: string;
  mode: "single-branch" | "sibling-set" | "post-merge-main";
  status: "passed" | "failed" | "conflict";
  summary: string;
  evidence: Prisma.InputJsonValue;
};

export async function recordLocalIntegrationResult(input: LocalIntegrationResultInput) {
  return recordExternalEvidence({
    actorUserId: input.actorUserId,
    routeContext: input.routeContext,
    operationType: "local_integration_ci",
    target: input.candidateBranch,
    provider: input.provider,
    resultSummary: input.summary,
    buildId: input.buildId,
    taskRunId: input.taskRunId,
    details: {
      externalSessionId: input.externalSessionId,
      mode: input.mode,
      status: input.status,
      evidence: input.evidence,
    },
  });
}
```

- [ ] **Step 6: Add MCP tool**

Add `record_local_integration_result` to `apps/web/lib/mcp-tools.ts`. It should call `recordLocalIntegrationResult` and return success with the evidence id.

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

Run:

```powershell
pnpm exec vitest run scripts/lib/local-integration-ci.test.mjs
pnpm --filter web exec vitest run lib/nonprod/local-integration.test.ts lib/mcp-tools-nonprod-environments.test.ts
```

Expected: PASS.

- [ ] **Step 8: Commit**

Run:

```powershell
git add scripts/lib/local-integration-ci.mjs scripts/lib/local-integration-ci.test.mjs scripts/local-integration-ci.mjs apps/web/lib/nonprod/local-integration.ts apps/web/lib/nonprod/local-integration.test.ts apps/web/lib/mcp-tools.ts
git commit -s -m "feat: add local integration CI runner"
```

## Task 7: Founder Review Queue Projection

**Files:**
- Create: `apps/web/lib/founder-review/queue.ts`
- Create: `apps/web/lib/founder-review/queue.test.ts`
- Create: `apps/web/app/(shell)/platform/ai/founder-review/page.tsx`
- Create: `apps/web/app/(shell)/platform/ai/founder-review/page.test.tsx`

- [ ] **Step 1: Write queue projection tests**

Create `apps/web/lib/founder-review/queue.test.ts`:

```ts
import { describe, expect, it } from "vitest";
import { projectFounderReviewCandidate } from "./queue";

describe("founder review queue", () => {
  it("projects unresolved decision interactions into review cards", () => {
    const candidate = projectFounderReviewCandidate({
      interactionId: "DI-1",
      question: "Should the interface hide raw traces?",
      options: ["Hide by default", "Show raw traces"],
      outcomeType: "defer",
      outcomePayload: { unresolvedReason: "principle-gap" },
      buildId: "FB-1",
      taskRunId: null,
      routeContext: "/build",
      createdAt: new Date("2026-05-26T12:00:00.000Z"),
    });
    expect(candidate.id).toBe("DI-1");
    expect(candidate.unresolvedReason).toBe("principle-gap");
    expect(candidate.unresolvedReasonLabel).toBe("Principle gap");
    expect(candidate.primaryActionLabel).toBe("Clarify founder principle");
    expect(candidate.links.buildHref).toBe("/build?buildId=FB-1");
  });
});
```

- [ ] **Step 2: Run and verify failure**

Run:

```powershell
pnpm --filter web exec vitest run lib/founder-review/queue.test.ts
```

Expected: FAIL because the queue projection does not exist.

- [ ] **Step 3: Create projection module**

Create `apps/web/lib/founder-review/queue.ts`:

```ts
export type FounderReviewUnresolvedReason =
  | "principle-gap"
  | "evidence-gap"
  | "domain-gap"
  | "ownership-gap"
  | "volunteers-dilemma";

type DecisionInteractionQueueRow = {
  interactionId: string;
  question: string;
  options: unknown;
  outcomeType: string;
  outcomePayload: unknown;
  buildId: string | null;
  taskRunId: string | null;
  routeContext: string | null;
  createdAt: Date;
};

function asRecord(value: unknown): Record<string, unknown> {
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

const ACTION_BY_REASON: Record<FounderReviewUnresolvedReason, string> = {
  "principle-gap": "Clarify founder principle",
  "evidence-gap": "Request better evidence",
  "domain-gap": "Route to domain owner",
  "ownership-gap": "Assign an accountable owner",
  "volunteers-dilemma": "Choose the responsible volunteer path",
};

const LABEL_BY_REASON: Record<FounderReviewUnresolvedReason, string> = {
  "principle-gap": "Principle gap",
  "evidence-gap": "Evidence gap",
  "domain-gap": "Domain gap",
  "ownership-gap": "Ownership gap",
  "volunteers-dilemma": "Volunteer's dilemma",
};

export function projectFounderReviewCandidate(row: DecisionInteractionQueueRow) {
  const payload = asRecord(row.outcomePayload);
  const unresolvedReason = typeof payload.unresolvedReason === "string"
    ? payload.unresolvedReason as FounderReviewUnresolvedReason
    : "principle-gap";
  return {
    id: row.interactionId,
    question: row.question,
    options: Array.isArray(row.options) ? row.options : [],
    unresolvedReason,
    unresolvedReasonLabel: LABEL_BY_REASON[unresolvedReason] ?? "Review needed",
    primaryActionLabel: ACTION_BY_REASON[unresolvedReason] ?? "Review decision",
    createdAt: row.createdAt.toISOString(),
    links: {
      buildHref: row.buildId ? `/build?buildId=${row.buildId}` : null,
      taskRunHref: row.taskRunId ? `/platform/ai/history?taskRunId=${row.taskRunId}` : null,
      routeContext: row.routeContext,
    },
  };
}
```

- [ ] **Step 4: Create initial page**

Create `apps/web/app/(shell)/platform/ai/founder-review/page.tsx` as a server component that queries `prisma.decisionInteraction.findMany` where `outcomeType` is `defer` or `escalate`, then renders cards through the projection. Use DPF theme variables for all colors.

Default page acceptance:

- Group cards by `unresolvedReasonLabel`, not by raw `outcomeType`.
- Each card shows question, reason label, primary action label, and build/task links when present.
- Default cards do not render `DecisionInteraction`, `outcomePayload`, `mcp`, `principle_decide`, or skill ids.
- Audit details are out of scope for this first page; do not expose them in the default card.

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

Run:

```powershell
pnpm --filter web exec vitest run lib/founder-review/queue.test.ts app/\\(shell\\)/platform/ai/founder-review/page.test.tsx
```

Expected: PASS.

- [ ] **Step 6: Commit**

Run:

```powershell
git add apps/web/lib/founder-review/queue.ts apps/web/lib/founder-review/queue.test.ts apps/web/app/`(shell`)/platform/ai/founder-review/page.tsx apps/web/app/`(shell`)/platform/ai/founder-review/page.test.tsx
git commit -s -m "feat: add founder review queue projection"
```

## Task 8: Build Studio UX Simplification and Evidence Timeline

**Files:**
- Create: `apps/web/components/build/EnvironmentStatusSummary.tsx`
- Create: `apps/web/components/build/EnvironmentStatusSummary.test.tsx`
- Create: `apps/web/components/build/UnifiedEvidenceTimeline.tsx`
- Create: `apps/web/components/build/UnifiedEvidenceTimeline.test.tsx`
- Modify: `apps/web/components/build/WorkflowStageInspector.tsx`
- Modify: `apps/web/components/build/WorkflowStageInspector.test.tsx`
- Modify: `apps/web/lib/explore/feature-build-types.ts`
- Modify: `apps/web/lib/explore/feature-build-data.ts`
- Modify: `apps/web/lib/explore/feature-build-data.test.ts`

- [ ] **Step 1: Write environment summary component test**

Create `apps/web/components/build/EnvironmentStatusSummary.test.tsx`:

```tsx
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { EnvironmentStatusSummary } from "./EnvironmentStatusSummary";

describe("EnvironmentStatusSummary", () => {
  it("shows shared environment readiness without raw trace details", () => {
    render(
      <EnvironmentStatusSummary
        activeCandidate={{ status: "available", url: "http://localhost:53601" }}
        localIntegration={{ status: "passed", summary: "Merged with origin/main and build passed." }}
      />,
    );
    expect(screen.getByText("Environment readiness")).toBeInTheDocument();
    expect(screen.getByText("Shared environment ready")).toBeInTheDocument();
    expect(screen.getByRole("link", { name: "Open environment" })).toHaveAttribute("href", "http://localhost:53601");
    expect(screen.getByText("Merged with origin/main and build passed.")).toBeInTheDocument();
    expect(screen.queryByText(/mcp/i)).toBeNull();
    expect(screen.queryByText(/lease/i)).toBeNull();
    expect(screen.queryByText(/localhost:53601/i)).toBeNull();
  });
});
```

- [ ] **Step 2: Run and verify failure**

Run:

```powershell
pnpm --filter web exec vitest run components/build/EnvironmentStatusSummary.test.tsx
```

Expected: FAIL because the component does not exist.

- [ ] **Step 3: Create environment summary**

Create `apps/web/components/build/EnvironmentStatusSummary.tsx`:

```tsx
type EnvironmentState = {
  status: "available" | "claimed" | "blocked" | "passed" | "failed";
  url?: string | null;
  summary?: string | null;
};

export function EnvironmentStatusSummary({
  activeCandidate,
  localIntegration,
}: {
  activeCandidate: EnvironmentState;
  localIntegration: EnvironmentState;
}) {
  const environmentMessage = activeCandidate.status === "blocked"
    ? "Shared environment blocked"
    : activeCandidate.status === "claimed"
      ? "Shared environment in use"
      : "Shared environment ready";
  return (
    <section className="rounded-md border border-[var(--dpf-border)] bg-[var(--dpf-surface-1)] p-3 text-[var(--dpf-text)]" aria-label="Environment readiness">
      <h3 className="text-sm font-semibold">Environment readiness</h3>
      <p className="mt-1 text-sm text-[var(--dpf-muted)]">
        {environmentMessage}
      </p>
      {activeCandidate.url ? (
        <a className="mt-2 inline-flex text-sm font-medium text-[var(--dpf-accent)]" href={activeCandidate.url}>
          Open environment
        </a>
      ) : null}
      <p className="mt-2 text-sm text-[var(--dpf-muted)]">
        {localIntegration.summary ?? "Local integration evidence is not recorded yet."}
      </p>
    </section>
  );
}
```

- [ ] **Step 4: Create unified evidence timeline test**

Create `apps/web/components/build/UnifiedEvidenceTimeline.test.tsx`:

```tsx
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { UnifiedEvidenceTimeline } from "./UnifiedEvidenceTimeline";

describe("UnifiedEvidenceTimeline", () => {
  it("labels external and local integration evidence by source", () => {
    render(
      <UnifiedEvidenceTimeline
        events={[
          { id: "1", source: "codex", label: "Codex", summary: "Unit tests passed." },
          { id: "2", source: "local-integration", label: "Local integration", summary: "Merged-code gate passed." },
        ]}
      />,
    );
    expect(screen.getByText("Codex")).toBeInTheDocument();
    expect(screen.getByText("Local integration")).toBeInTheDocument();
    expect(screen.queryByText(/ExternalEvidenceRecord/i)).toBeNull();
    expect(screen.queryByText(/ToolExecutionReceipt/i)).toBeNull();
  });
});
```

- [ ] **Step 5: Create unified evidence timeline**

Create `apps/web/components/build/UnifiedEvidenceTimeline.tsx`:

```tsx
export type UnifiedEvidenceTimelineEvent = {
  id: string;
  source: "build-studio" | "claude" | "codex" | "coworker" | "local-integration";
  label: string;
  summary: string;
};

export function UnifiedEvidenceTimeline({ events }: { events: UnifiedEvidenceTimelineEvent[] }) {
  return (
    <ol className="space-y-2">
      {events.map((event) => (
        <li key={event.id} className="rounded-md border border-[var(--dpf-border)] bg-[var(--dpf-surface-1)] p-3">
          <div className="text-sm font-semibold text-[var(--dpf-text)]">{event.label}</div>
          <div className="mt-1 text-sm text-[var(--dpf-muted)]">{event.summary}</div>
        </li>
      ))}
    </ol>
  );
}
```

- [ ] **Step 6: Wire into Build Studio inspector**

Modify `apps/web/components/build/WorkflowStageInspector.tsx`:

- Import `EnvironmentStatusSummary`.
- Import `UnifiedEvidenceTimeline`.
- Place the environment summary below the primary action card.
- Place timeline behind the existing evidence/details area, not above the primary recommendation.
- Preserve one primary action above the fold. Evidence, skill ids, MCP receipts, and local integration logs must not compete with the recommendation.
- Reuse the existing Build Studio status/action card language where possible so the page does not gain a second narrator.

- [ ] **Step 7: Run component tests**

Run:

```powershell
pnpm --filter web exec vitest run components/build/EnvironmentStatusSummary.test.tsx components/build/UnifiedEvidenceTimeline.test.tsx components/build/WorkflowStageInspector.test.tsx
```

Expected: PASS.

- [ ] **Step 8: Commit**

Run:

```powershell
git add apps/web/components/build/EnvironmentStatusSummary.tsx apps/web/components/build/EnvironmentStatusSummary.test.tsx apps/web/components/build/UnifiedEvidenceTimeline.tsx apps/web/components/build/UnifiedEvidenceTimeline.test.tsx apps/web/components/build/WorkflowStageInspector.tsx apps/web/components/build/WorkflowStageInspector.test.tsx apps/web/lib/explore/feature-build-types.ts apps/web/lib/explore/feature-build-data.ts apps/web/lib/explore/feature-build-data.test.ts
git commit -s -m "feat: simplify Build Studio evidence surface"
```

## Task 9: Final Verification and Local Integration Gate

**Files:**
- No source files expected unless verification exposes a defect.

- [ ] **Step 1: Run focused tests**

Run:

```powershell
pnpm --filter @dpf/db exec vitest run src/seed-skills.test.ts
pnpm --filter web exec vitest run lib/build/decision-service.test.ts lib/build/capability-packs.test.ts lib/actions/external-evidence.test.ts lib/nonprod/environment-lease.test.ts lib/nonprod/local-integration.test.ts lib/founder-review/queue.test.ts components/build/EnvironmentStatusSummary.test.tsx components/build/UnifiedEvidenceTimeline.test.tsx
```

Expected: PASS.

- [ ] **Step 2: Run typecheck**

Run:

```powershell
pnpm --filter web typecheck
pnpm --filter @dpf/db typecheck
```

Expected: PASS.

- [ ] **Step 3: Run production build**

Run:

```powershell
cd apps/web
pnpm exec next build
```

Expected: PASS.

- [ ] **Step 4: Apply migrations locally**

Run:

```powershell
pnpm --filter @dpf/db exec prisma migrate deploy
pnpm --filter @dpf/db exec prisma validate
```

Expected: PASS.

- [ ] **Step 5: Run local integration CI before push**

Run from repo root:

```powershell
$branch = git branch --show-current
node scripts/local-integration-ci.mjs --candidate $branch --mode single-branch
```

Expected: PASS. The branch merges into current `origin/main`; web Vitest, typecheck, and `next build` pass in the local integration lane.

- [ ] **Step 6: Verify UX against the running app**

Use Browser against the governed `localhost` environment. Verify:

- Build Studio phase surface shows current state, recommendation, confidence/evidence summary, environment status, and one primary action above the fold.
- Default Build Studio surface does not show raw MCP names, skill ids, lease ids, `DecisionInteraction`, `ExternalEvidenceRecord`, or literal `unknown`/`null` state labels.
- "View audit" or the existing evidence/details affordance reveals skill IDs, external evidence, local integration evidence, and founder review link without changing the primary recommendation.
- Founder Review Queue lists unresolved WWMD decisions grouped by human-readable reason and shows one primary action per card.
- Route-to-route links preserve build/task context; the verifier does not need to copy an id from one page to another.
- Narrow viewport does not overlap text or push the primary action below unrelated evidence details.
- No extra server is started by the verification session.

- [ ] **Step 7: Push branch**

Run:

```powershell
git push
```

Expected: branch is pushed after merged-code local integration evidence is green.

## PR Readiness Checklist

- [ ] New skill files pass seed mirror invariant.
- [ ] Capability pack metadata loads and references existing skill IDs.
- [ ] Build Studio decision service handles recommend, needs-human, captured-gap, and blocked states.
- [ ] Decision-service results include operator-facing action labels that do not leak MCP/tool implementation language.
- [ ] External evidence intake records build/task links and local integration details.
- [ ] Non-production environment leases enforce one active owner per environment.
- [ ] Local integration CI blocks push/PR on merge conflict or gate failure.
- [ ] Founder Review Queue projects unresolved decisions into human-readable groups with one primary action per card.
- [ ] Build Studio UI stays recommendation-first, theme-aware, and free of raw trace vocabulary in the default view.
- [ ] Evidence/timeline details remain available behind audit drill-downs.
- [ ] No unmanaged per-thread server is started during verification.
- [ ] Focused tests pass.
- [ ] Typecheck passes.
- [ ] Production build passes.
- [ ] Migrations apply cleanly.
- [ ] UX verification passes against `localhost`.
