"""GitHub Actions executor for codegen runs.

Core remains authoritative for authorization, billing, run state, and finalization.
This module only prepares a repo-local workflow payload and dispatches GitHub
Actions to perform the expensive repo edit work.
"""
# ruff: noqa: E501
from __future__ import annotations

import hashlib
import hmac
import json
from datetime import datetime, timezone
from typing import Any, Optional
from uuid import UUID

import structlog

from src.config import AIProvider, get_settings

logger = structlog.get_logger()

WORKFLOW_PATH = ".github/workflows/p2hs-codegen.yml"
RUNNER_PATH = "scripts/p2hs-codegen-action-runner.mjs"
PAYLOAD_DIR = ".p2hs/codegen/runs"


def _json_dumps(data: Any, *, pretty: bool = True) -> str:
    if pretty:
        return json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)
    return json.dumps(data, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def _sign_payload(payload: dict[str, Any], secret: str) -> str:
    body = _json_dumps(payload, pretty=False).encode("utf-8")
    digest = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
    return f"sha256={digest}"


def _provider_secret_name(provider: str) -> Optional[str]:
    provider = (provider or "").lower()
    if provider == AIProvider.CURSOR.value:
        return "CURSOR_API_KEY"
    if provider == AIProvider.OPENAI.value:
        return "OPENAI_API_KEY"
    if provider == AIProvider.CODEX.value:
        return "OPENAI_API_KEY"
    if provider == AIProvider.CLAUDE.value:
        return "ANTHROPIC_API_KEY"
    return None


def _provider_secret_value(provider: str) -> str:
    settings = get_settings()
    provider = (provider or "").lower()
    if provider == AIProvider.CURSOR.value:
        return settings.cursor_api_key
    if provider == AIProvider.OPENAI.value:
        return settings.openai_api_key
    if provider == AIProvider.CODEX.value:
        return settings.openai_api_key
    if provider == AIProvider.CLAUDE.value:
        return settings.anthropic_api_key
    return ""


def build_codegen_actions_workflow(project_type: str = "", project_ref: str = "", *, w4p: bool = False) -> str:
    """Codegen workflow. The generated app's deploy.yml owns the build upload."""
    del project_type, project_ref, w4p
    deploy_job = ""
    return f"""name: P2HS Codegen

on:
  workflow_dispatch:
    inputs:
      run_id:
        description: P2HS codegen run id
        required: true
      payload_path:
        description: Signed P2HS payload path
        required: true
      cursor_model:
        description: Cursor model override
        required: false
      openai_model:
        description: OpenAI model override
        required: false
      claude_model:
        description: Claude model override
        required: false
      provider:
        description: Codegen provider override
        required: false
      continuation_attempt:
        description: Save-and-restart attempt number
        required: false

permissions:
  contents: write

concurrency:
  group: p2hs-codegen-${{{{ github.ref }}}}
  cancel-in-progress: false

jobs:
  codegen:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    outputs:
      commit_sha: ${{{{ steps.codegen.outputs.commit_sha }}}}
      continued: ${{{{ steps.codegen.outputs.continued }}}}
      deploy_required: ${{{{ steps.codegen.outputs.deploy_required }}}}
      deploy_branch: ${{{{ steps.codegen.outputs.deploy_branch }}}}
    env:
      P2HS_CODEGEN_CALLBACK_TOKEN: ${{{{ secrets.P2HS_CODEGEN_CALLBACK_TOKEN }}}}
      P2HS_CODEGEN_PAYLOAD_SECRET: ${{{{ secrets.P2HS_CODEGEN_PAYLOAD_SECRET }}}}
      CURSOR_API_KEY: ${{{{ secrets.CURSOR_API_KEY }}}}
      OPENAI_API_KEY: ${{{{ secrets.OPENAI_API_KEY }}}}
      ANTHROPIC_API_KEY: ${{{{ secrets.ANTHROPIC_API_KEY }}}}
      CLOUDFLARE_API_TOKEN: ${{{{ secrets.CLOUDFLARE_API_TOKEN }}}}
      CLOUDFLARE_ACCOUNT_ID: ${{{{ secrets.CLOUDFLARE_ACCOUNT_ID }}}}
      CURSOR_CLI_PATH: ${{{{ vars.CURSOR_CLI_PATH || 'agent' }}}}
      CURSOR_TIMEOUT: ${{{{ vars.CURSOR_TIMEOUT || '1320' }}}}
      CURSOR_MODEL: ${{{{ inputs.cursor_model }}}}
      OPENAI_MODEL: ${{{{ inputs.openai_model || 'gpt-4o' }}}}
      CLAUDE_MODEL: ${{{{ inputs.claude_model || 'claude-sonnet-4-20250514' }}}}
      CODEGEN_PROVIDER: ${{{{ inputs.provider || 'cursor' }}}}
    steps:
      - name: Record job start
        run: echo "P2HS_JOB_STARTED_AT=$(date +%s)" >> "$GITHUB_ENV"
      - uses: actions/checkout@v5
        with:
          ref: ${{{{ github.ref }}}}
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: Install Cursor agent
        if: ${{{{ env.CODEGEN_PROVIDER == 'cursor' && env.CURSOR_API_KEY != '' }}}}
        run: |
          curl https://cursor.com/install -fsS | bash
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - name: Install OpenAI Codex CLI
        if: ${{{{ env.CODEGEN_PROVIDER == 'codex' }}}}
        run: |
          curl -fsSL https://chatgpt.com/codex/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - name: Install Claude Code CLI
        if: ${{{{ env.CODEGEN_PROVIDER == 'claude' }}}}
        run: |
          curl -fsSL https://claude.ai/install.sh | bash
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - name: Run P2HS codegen
        id: codegen
        run: node scripts/p2hs-codegen-action-runner.mjs "${{{{ inputs.payload_path }}}}"
{deploy_job}
"""


def build_codegen_actions_runner() -> str:
    from pathlib import Path

    from src.domain.codegen.legacy_store_retire import shadow_store_stubs

    auth_conformance = Path(__file__).with_name("generated_auth_conformance.py").read_text(encoding="utf-8")
    guard = Path(__file__).with_name("backend_guard.py").read_text(encoding="utf-8")
    # The Actions runner executes inside the generated app checkout, where
    # Core's ``src.domain`` package is unavailable.  Embed the auth helpers
    # before the guard and remove the second future import so the combined
    # source remains valid as a single ``python -c`` program.
    guard = auth_conformance + "\n" + guard.replace(
        "from __future__ import annotations\n", "", 1
    )
    return r"""#!/usr/bin/env node
import { createHmac } from "node:crypto";
import { spawn } from "node:child_process";
import { appendFile, mkdir, mkdtemp, readFile, writeFile, rm, rename, readdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import path from "node:path";

const SHADOW_STUBS = __P2HS_SHADOW_STUBS__;

const payloadPath = process.argv[2];
if (!payloadPath) {
  console.error("payload path is required");
  process.exit(2);
}

let activePayload = null;
const progressSends = [];
const CALLBACK_ATTEMPTS = 5;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function normalizeCallbackToken(value) {
  let token = String(value || "").trim();
  if (token.slice(0, 7).toLowerCase() === "bearer ") token = token.slice(7).trim();
  return token;
}
function callbackAuthToken(payload) {
  return normalizeCallbackToken(payload?.callback_grant || process.env.P2HS_CODEGEN_CALLBACK_TOKEN);
}
function callbackAuthTokens(payload) {
  return [...new Set([
    normalizeCallbackToken(payload?.callback_grant),
    normalizeCallbackToken(process.env.P2HS_CODEGEN_CALLBACK_TOKEN),
  ].filter(Boolean))];
}

function progressUrl(payload) {
  if (payload.progress_url) return payload.progress_url;
  return String(payload.callback_url || "").replace(/\/complete$/, "/progress");
}

function postProgress(payload, text, stream = "stdout", detailFormat = null) {
  const url = progressUrl(payload);
  if (!url || !callbackAuthToken(payload)) return;
  const body = { text: String(text).slice(0, 12000), stream };
  if (detailFormat) body.detail_format = detailFormat;
  const p = fetch(url, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${callbackAuthToken(payload)}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  }).catch(() => {});
  progressSends.push(p);
}

function log(message) {
  console.log(`[p2hs-codegen] ${message}`);
  if (activePayload) postProgress(activePayload, `[p2hs-codegen] ${message}`);
}

async function writeGithubOutput({ commitSha = "", continued = false, deployRequired = false, deployBranch = "" }) {
  if (!process.env.GITHUB_OUTPUT) return;
  await appendFile(
    process.env.GITHUB_OUTPUT,
    `commit_sha=${commitSha}\ncontinued=${continued ? "true" : "false"}\ndeploy_required=${deployRequired ? "true" : "false"}\ndeploy_branch=${deployBranch}\n`,
    "utf8",
  );
}

function deployArtifactPath() {
  return path.join(process.env.RUNNER_TEMP || ".p2hs", "action-result.json");
}

async function saveDeployArtifact(resultBody) {
  const artifactPath = deployArtifactPath();
  await mkdir(path.dirname(artifactPath), { recursive: true });
  const body = JSON.stringify(resultBody);
  await writeFile(artifactPath, body, "utf8");
  if (path.resolve(artifactPath) !== path.resolve(".p2hs/action-result.json")) {
    await mkdir(".p2hs", { recursive: true });
    await writeFile(".p2hs/action-result.json", body, "utf8");
  }
  log("Preparing preview deployment");
}

const exec = (cmd, args, options = {}) => new Promise((resolve) => {
  const child = spawn(cmd, args, {
    cwd: options.cwd || process.cwd(),
    env: { ...process.env, ...(options.env || {}) },
    shell: false,
  });
  if (options.input !== undefined) child.stdin.end(String(options.input));
  const timeoutMs = Number(options.timeoutMs || process.env.GIT_TIMEOUT_MS || 120000);
  const stdout = [];
  const stderr = [];
  let outBuffer = "";
  let errBuffer = "";
  let timedOut = false;

  const flushLines = (chunk, stream) => {
    const text = chunk.toString("utf8");
    if (stream === "stderr") {
      if (!options.quiet) process.stderr.write(text);
      stderr.push(text);
      errBuffer += text;
    } else {
      if (!options.quiet) process.stdout.write(text);
      stdout.push(text);
      outBuffer += text;
    }
    const buffer = stream === "stderr" ? errBuffer : outBuffer;
    const parts = buffer.split(/\r?\n/);
    const rest = parts.pop() || "";
    for (const line of parts) {
      if (!options.quiet && activePayload && line.trim()) {
        postProgress(activePayload, line, stream, options.detailFormat || null);
      }
    }
    if (stream === "stderr") errBuffer = rest;
    else outBuffer = rest;
  };

  let killTimer;
  const timer = setTimeout(() => {
    timedOut = true;
    try { child.kill("SIGTERM"); } catch {}
    killTimer = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, 10000);
  }, timeoutMs);

  child.stdout?.on("data", (chunk) => flushLines(chunk, "stdout"));
  child.stderr?.on("data", (chunk) => flushLines(chunk, "stderr"));
  child.on("error", (error) => {
    clearTimeout(timer);
    resolve({ code: error?.code ?? 1, stdout: stdout.join(""), stderr: `${stderr.join("")}\n${error.message}` });
  });
  child.on("close", (code) => {
    clearTimeout(killTimer);
    clearTimeout(timer);
    if (!options.quiet && outBuffer.trim() && activePayload) postProgress(activePayload, outBuffer, "stdout", options.detailFormat || null);
    if (!options.quiet && errBuffer.trim() && activePayload) postProgress(activePayload, errBuffer, "stderr", options.detailFormat || null);
    resolve({ code: timedOut ? 124 : (code ?? 0), stdout: stdout.join(""), stderr: stderr.join("") });
  });
});

function extractCursorSessionId(text) {
  const keys = ["session_id", "chat_id", "conversation_id", "thread_id", "cursor_session_id"];
  let last = "";
  for (const raw of String(text || "").split(/\r?\n/)) {
    const line = raw.trim();
    if (!line.startsWith("{")) continue;
    try {
      const obj = JSON.parse(line);
      const candidates = [obj];
      if (obj && typeof obj.data === "object") candidates.push(obj.data);
      if (obj && typeof obj.message === "object") candidates.push(obj.message);
      for (const c of candidates) {
        if (!c || typeof c !== "object") continue;
        for (const key of keys) {
          const v = c[key];
          if (typeof v === "string" && v.trim().length >= 8) last = v.trim();
        }
      }
    } catch {}
  }
  return last;
}

async function installP2hsGitHook() {
  await mkdir(".p2hs", { recursive: true });
  await writeFile(".p2hs/commit-msg-hook.py", `import pathlib, re, sys
p = pathlib.Path(sys.argv[1])
t = p.read_text(encoding="utf-8")
t = re.sub(r"(?im)^\\s*Co-authored-by:\\s*Cursor.*\\n?", "", t)
t = re.sub(r"(?im)^\\s*Made-with:\\s*Cursor.*\\n?", "", t)
t = re.sub(r"(?im)^\\s*Co-authored-by:.*cursoragent@cursor\\.com.*\\n?", "", t)
if "Authored by P2HS only" not in t:
    t = t.rstrip() + "\\n\\nAuthored by P2HS only\\n"
p.write_text(t, encoding="utf-8")
`);
  const hookPath = path.join(".git", "hooks", "commit-msg");
  await mkdir(path.dirname(hookPath), { recursive: true });
  // Inline Python so commit still works after ephemeral files are removed.
  await writeFile(hookPath, `#!/bin/sh
python3 - "$1" <<'PY'
import pathlib, re, sys
p = pathlib.Path(sys.argv[1])
t = p.read_text(encoding="utf-8")
t = re.sub(r"(?im)^\\s*Co-authored-by:\\s*Cursor.*\\n?", "", t)
t = re.sub(r"(?im)^\\s*Made-with:\\s*Cursor.*\\n?", "", t)
t = re.sub(r"(?im)^\\s*Co-authored-by:.*cursoragent@cursor\\.com.*\\n?", "", t)
if "Authored by P2HS only" not in t:
    t = t.rstrip() + "\\n\\nAuthored by P2HS only\\n"
p.write_text(t, encoding="utf-8")
PY
`, { mode: 0o755 });
}

async function installCursorCliConfig(name, email) {
  const home = process.env.HOME || homedir();
  const dir = path.join(home, ".cursor");
  await mkdir(dir, { recursive: true });
  await writeFile(path.join(dir, "cli-config.json"), JSON.stringify({
    attribution: { attributeCommitsToAgent: false, attributePRsToAgent: false },
    commitAttribution: false,
    prAttribution: false,
  }, null, 2), "utf8");
  process.env.GIT_AUTHOR_NAME = name;
  process.env.GIT_AUTHOR_EMAIL = email;
  process.env.GIT_COMMITTER_NAME = name;
  process.env.GIT_COMMITTER_EMAIL = email;
}

async function rewriteCommitsAuthoredByP2hs(baseHead, name, email) {
  const counted = await exec("git", ["rev-list", "--count", `${baseHead}..HEAD`]);
  const n = Number(String(counted.stdout || "0").trim());
  if (!n) return;
  const dirty = await exec("git", ["status", "--porcelain"]);
  if (dirty.stdout.trim()) {
    log("authorship rewrite skipped: working tree is not clean");
    return;
  }
  // filter-branch checks out each commit into .git-rewrite/t and runs
  // --msg-filter there. A repo-relative script is therefore missing.
  const rewriteDir = await mkdtemp(path.join(tmpdir(), "p2hs-rewrite-"));
  const scriptPath = path.join(rewriteDir, "rewrite-commit-msg.mjs");
  await writeFile(scriptPath, `#!/usr/bin/env node
import fs from "node:fs";
let t = fs.readFileSync(0, "utf8");
t = t.replace(/^\\s*Co-authored-by:\\s*Cursor.*\\n?/gim, "");
t = t.replace(/^\\s*Made-with:\\s*Cursor.*\\n?/gim, "");
t = t.replace(/^\\s*Co-authored-by:.*cursoragent@cursor\\.com.*\\n?/gim, "");
if (!/Authored by P2HS only/i.test(t)) {
  t = t.replace(/\\s*$/, "\\n\\nAuthored by P2HS only\\n");
}
process.stdout.write(t);
`);
  const rewritten = await exec("git", [
    "filter-branch", "-f",
    "--env-filter",
    `export GIT_AUTHOR_NAME=${JSON.stringify(name)}; export GIT_AUTHOR_EMAIL=${JSON.stringify(email)}; export GIT_COMMITTER_NAME=${JSON.stringify(name)}; export GIT_COMMITTER_EMAIL=${JSON.stringify(email)};`,
    "--msg-filter", `node ${JSON.stringify(scriptPath)}`,
    `${baseHead}..HEAD`,
  ], {
    env: { FILTER_BRANCH_SQUELCH_WARNING: "1" },
    timeoutMs: 180000,
  });
  try { await rm(rewriteDir, { recursive: true, force: true }); } catch {}
  if (rewritten.code !== 0) {
    log(`authorship rewrite skipped: ${String(rewritten.stderr || rewritten.stdout || "filter-branch failed").replace(/\s+/g, " ").trim().slice(0, 300)}`);
  }
}

function p2hsCommitMessage(message) {
  const text = String(message || "p2hs: apply codegen changes").trim();
  return /Authored by P2HS only/i.test(text) ? text : `${text}\n\nAuthored by P2HS only`;
}

function stableStringify(value) {
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
  if (value && typeof value === "object") {
    return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
  }
  return JSON.stringify(value);
}

function normalizeRepoPath(value) {
  return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "").trim();
}

const CONTROL_PATH_PREFIXES = [".p2hs/", ".p2hs-control/", "p2hs/", ".cursor/"];
const CONTROL_PATHS = new Set([
  ".p2hs.json", ".cursorrules", "CLAUDE.md", "AGENTS.md", "p2hs.platform.json",
  "p2hs/project-brief.md", "p2hs/app-blocks-plan.json",
  "p2hs/discovery-cloud-contract.json", "p2hs/project-state.json", "p2hs/runtime-contract.json",
  "p2hs/runtime-dependencies.json", "p2hs/prompt-media",
]);
const SECRET_NAME_MARKERS = ["api_key", "apikey", "secret", "password", "private_key", "service_token", "cloud_service_token"];
const PHYSICAL_HOST_PATTERNS = [
  /cloudgate\.p2hs\.com/i,
  /stag-cloudgate\.p2hs\.com/i,
  /https?:\/\/[^/\s]*cloud\.p2hs\.com/i,
  /https?:\/\/[^/\s]*p2hs\.cloud/i,
];
const PUBLIC_BFF_ENV_MARKERS = [
  "NUXT_PUBLIC_P2HS_GATEWAY_URL", "VITE_P2HS_GATEWAY_URL",
  "NUXT_PUBLIC_P2HS_CLOUD_PUBLIC_URL", "VITE_P2HS_CLOUD_PUBLIC_URL",
  "NUXT_PUBLIC_P2HS_CORE_PUBLIC_URL", "VITE_P2HS_CORE_PUBLIC_URL",
  "NEXT_PUBLIC_P2HS_BFF_URL", "REACT_APP_BFF_URL",
];
const PUBLISHABLE_SUFFIXES = [
  ".vue", ".jsx", ".tsx", ".js", ".ts", ".mjs", ".cjs", ".css", ".scss", ".html",
  ".json", ".md", ".svg", ".png", ".jpg", ".jpeg", ".webp", ".woff", ".woff2",
  ".txt", ".yml", ".yaml",
];
const PUBLISHABLE_NAMES = new Set([
  "package.json", "package-lock.json", "pnpm-lock.yaml", "tsconfig.json",
  "nuxt.config.ts", "nuxt.config.js", "vite.config.ts", "vite.config.js",
  "wrangler.toml", "wrangler.jsonc", ".gitignore", "README.md", "index.html",
  "app.vue", "app.jsx", "app.tsx", "main.ts", "main.js", "error.vue",
]);
const PUBLISHABLE_PREFIXES = [
  "app/", "src/", "pages/", "components/", "layouts/", "public/", "server/",
  "functions/", "assets/", "styles/", "css/", "content/", "middleware/",
  "plugins/", "composables/", "lib/", "utils/", "types/", "tests/", "e2e/",
  "static/", "js/", ".github/",
];

function pathBasename(relative) {
  const parts = String(relative || "").split("/");
  return parts[parts.length - 1] || "";
}

function isForbiddenPath(value) {
  const relative = normalizeRepoPath(value);
  const lowered = relative.toLowerCase();
  const name = pathBasename(lowered);
  if (!relative) return true;
  if (CONTROL_PATHS.has(relative) || CONTROL_PATH_PREFIXES.some((prefix) => relative.startsWith(prefix))) return true;
  if (lowered.includes(".p2hs/ai-context/") || lowered.includes("p2hs/prompt-media/") || lowered.includes(".p2hs/local-bak/") || lowered.includes(".p2hs-local.bak")) return true;
  if (SECRET_NAME_MARKERS.some((marker) => name.includes(marker)) && (name.endsWith(".pem") || name.endsWith(".key") || name.endsWith(".env") || name.endsWith(".json"))) return true;
  if (name === ".env" || name === ".env.local" || name === ".env.production") return true;
  return false;
}

function isAllowlistedPath(value) {
  if (isForbiddenPath(value)) return false;
  const relative = normalizeRepoPath(value);
  const name = pathBasename(relative);
  if (PUBLISHABLE_NAMES.has(name)) return true;
  if (PUBLISHABLE_PREFIXES.some((prefix) => relative.startsWith(prefix))) return true;
  if (name === "dockerfile" || name === "Makefile" || name === "makefile" || relative.startsWith(".github/")) return true;
  if (PUBLISHABLE_SUFFIXES.some((suffix) => relative.endsWith(suffix))) return true;
  return false;
}

function scanTextForLeaks(relative, text) {
  const hits = [];
  const lowered = String(text || "").toLowerCase();
  if (PHYSICAL_HOST_PATTERNS.some((pattern) => pattern.test(text))) hits.push(`${relative}: physical_cloud_hostname`);
  if (PUBLIC_BFF_ENV_MARKERS.some((marker) => text.includes(marker))) hits.push(`${relative}: public_bff_env`);
  if (["you are a codegen", "system prompt", "p2hs generation instructions", "architecture policy prompt", "## composition authority"].some((marker) => lowered.includes(marker)) && isForbiddenPath(relative)) {
    hits.push(`${relative}: generation_prompt`);
  }
  if (text.includes("BEGIN PRIVATE KEY") || text.includes("CLOUD_SERVICE_TOKEN=")) hits.push(`${relative}: secret_material`);
  return hits;
}

async function porcelainPaths() {
  const status = await exec("git", ["status", "--porcelain", "-uall"]);
  return status.stdout.split(/\r?\n/).map((line) => {
    if (line.length < 4) return "";
    let rest = line.slice(3);
    if (rest.includes(" -> ")) rest = rest.split(" -> ")[1];
    return normalizeRepoPath(rest.replace(/^"|"$/g, ""));
  }).filter(Boolean);
}

async function stagePublishableAndAssert() {
  const allowed = (await porcelainPaths()).filter((item) => isAllowlistedPath(item));
  for (let index = 0; index < allowed.length; index += 32) {
    const chunk = allowed.slice(index, index + 32);
    const added = await exec("git", ["add", "--", ...chunk]);
    if (added.code !== 0) throw new Error(`git add failed: ${added.stderr || added.stdout}`);
  }
  const cached = await exec("git", ["diff", "--cached", "--name-only"]);
  let staged = cached.stdout.split(/\r?\n/).map((line) => normalizeRepoPath(line)).filter(Boolean);
  const allowedSet = new Set(allowed);
  const drop = staged.filter((item) => isForbiddenPath(item) || !allowedSet.has(item));
  if (drop.length) {
    await exec("git", ["rm", "-f", "--cached", "--", ...drop]);
    staged = staged.filter((item) => !drop.includes(item));
  }
  const leaks = [];
  for (const relative of staged) {
    if (isForbiddenPath(relative)) {
      leaks.push(`${relative}: control_or_secret_path`);
      continue;
    }
    try {
      leaks.push(...scanTextForLeaks(relative, await readFile(relative, "utf8")));
    } catch {}
  }
  if (leaks.length) throw new Error("repo export rejected: " + leaks.slice(0, 12).join("; "));
  return staged;
}

function isInternalCodegenPath(value) {
  const normalized = normalizeRepoPath(value);
  if (!normalized) return false;
  if (isForbiddenPath(normalized)) return true;
  if (normalized === ".p2hs/local-result.json") return true;
  if (normalized.startsWith(".p2hs/ai-context/")) return true;
  if (normalized === ".p2hs/commit-msg-hook.py") return true;
  if (normalized === ".p2hs/rewrite-commit-msg.mjs") return true;
  return false;
}

function isPlatformProjectionPath(value, payload) {
  const normalized = normalizeRepoPath(value);
  if (!normalized) return false;
  if (normalized.startsWith(".p2hs/") || normalized.startsWith(".github/")) return true;
  if (normalized === "scripts/p2hs-codegen-action-runner.mjs") return true;
  if ([
    "p2hs/platform-ports.lock.json",
    "p2hs/cloud-contract.json",
    "p2hs/runtime-contract.json",
    "p2hs/admin-presentation.json",
    "p2hs/app-blocks-plan.json",
    "p2hs/discovery-cloud-contract.json",
    "p2hs/project-state.json",
    "p2hs/project-brief.md",
  ].includes(normalized)) return true;
  const files = (payload && (payload.record_adapter_files || payload.platform_adapter_files)) || {};
  if (Object.prototype.hasOwnProperty.call(files, normalized)) return true;
  const base = normalized.split("/").pop();
  return base === "p2hs-port-contract.mjs" || base === "p2hs-ports.mjs" || base === "p2hs-app.js" || base === "useP2hs.ts";
}

async function restoreEphemeralCodegenFiles(baseHead) {
  const source = String(baseHead || "HEAD").trim() || "HEAD";
  const candidates = [
    ".cursorrules",
    ".p2hs/ai-context/system-prompt.md",
    ".p2hs/ai-context/agent-prompt.md",
    ".p2hs/ai-context/last-run.json",
    ".p2hs/commit-msg-hook.py",
    ".p2hs/rewrite-commit-msg.mjs",
  ];
  for (const file of candidates) {
    try {
      const inBase = await exec("git", ["cat-file", "-e", `${source}:${file}`], {
        allowFailure: true,
        quiet: true,
      });
      if (inBase.code === 0) {
        await exec("git", ["restore", "--source", source, "--staged", "--worktree", "--", file], {
          allowFailure: true,
          quiet: true,
        });
        continue;
      }
      await exec("git", ["rm", "-f", "--ignore-unmatch", "--cached", "--", file], {
        allowFailure: true,
        quiet: true,
      });
      if (existsSync(file)) await rm(file, { force: true });
    } catch {}
  }
}

// ---- Run-scoped work branch (Core: src/domain/codegen/work_branch.py) ------------------------
// With a work branch the runner works on p2hs/codegen/<run_id>, pushes checkpoints to it (never
// force), and only advances `preview` (payload.branch) after Core-owned validation passes. A
// failed run leaves the branch, and every report carries where the last checkpoint is.
const WORK_BRANCH_RE = new RegExp("^p2hs/codegen/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
const SHA_RE = new RegExp("^[0-9a-f]{40}$");
// Capability manifests the gate reads; normally untracked, so a checkpoint must add them
// explicitly or a resumed run would have nothing to validate.
const CHECKPOINT_MANIFESTS = [
  "p2hs/feature-packs.json", "p2hs/provision.json",
  "p2hs/required-user-inputs.json", "p2hs/email-templates.json",
];
let activeWork = null;

function workBranchOf(payload) {
  const name = String(payload?.work_branch_name || "");
  return WORK_BRANCH_RE.test(name) && name === `p2hs/codegen/${payload.run_id}` ? name : null;
}

function workFields() {
  if (!activeWork) return {};
  return {
    work_branch: activeWork.branch,
    checkpoint: activeWork.checkpoint || undefined,
    checkpoint_sha: activeWork.sha || undefined,
  };
}

// Commit the manifests and push HEAD to the run branch. Plain push: the run branch only ever
// moves forward. Returns the pushed sha, or null (logged) if it could not be saved.
async function pushCheckpoint(payload, branch, label) {
  try {
    for (const file of CHECKPOINT_MANIFESTS) {
      if (existsSync(file)) await exec("git", ["add", "-f", "--", file], { quiet: true });
    }
    const staged = await exec("git", ["diff", "--cached", "--quiet"], { quiet: true });
    if (staged.code !== 0) {
      const message = `p2hs: checkpoint (${label}) run ${String(payload.run_id).slice(0, 8)}`;
      const committed = await exec("git", ["commit", "--no-verify", "-m", message], { quiet: true });
      if (committed.code !== 0) {
        log(`checkpoint commit failed: ${String(committed.stderr).slice(0, 300)}`);
        return null;
      }
    }
    const pushed = await exec(
      "git", ["push", "origin", `HEAD:refs/heads/${branch}`], { timeoutMs: 180000, quiet: true },
    );
    if (pushed.code !== 0) {
      log(`checkpoint push failed: ${String(pushed.stderr).slice(0, 300)}`);
      return null;
    }
    return (await exec("git", ["rev-parse", "HEAD"], { quiet: true })).stdout.trim();
  } catch (error) {
    log(`checkpoint failed: ${String(error?.message || error).slice(0, 300)}`);
    return null;
  }
}

// The run is failing. If the agent already changed files (committed or not) and nothing has been
// saved yet, keep that work on the run branch so a retry builds on it instead of starting over.
// Only what a normal commit would publish is kept (the same allowlist and leak scan); a leak
// means the uncommitted files are left out, never that the run fails a second time. Never throws.
async function saveWorkOnFailure(payload) {
  if (!activeWork || activeWork.checkpoint) return;
  try {
    const dirty = await exec("git", ["status", "--porcelain"], { quiet: true });
    if (dirty.stdout.trim()) {
      try {
        const staged = await stagePublishableAndAssert();
        if (staged.length) {
          const message = p2hsCommitMessage(`p2hs: save work in progress (run ${String(payload.run_id).slice(0, 8)})`);
          await exec("git", ["commit", "--no-verify", "-m", message], { quiet: true });
        }
      } catch (error) {
        log(`unsaved files were left out of the recovery commit: ${String(error?.message || error).slice(0, 200)}`);
        await exec("git", ["reset", "-q"], { quiet: true });
      }
    }
    const head = (await exec("git", ["rev-parse", "HEAD"], { quiet: true })).stdout.trim();
    if (!SHA_RE.test(head) || head === activeWork.changesBase) return; // nothing new to keep
    const saved = await pushCheckpoint(payload, activeWork.branch, "work in progress");
    if (saved) {
      activeWork.checkpoint = "partial_work";
      activeWork.sha = saved;
      log(`saved work in progress to ${activeWork.branch}`);
    }
  } catch (error) {
    log(`could not save work in progress: ${String(error?.message || error).slice(0, 300)}`);
  }
}

// Advance preview to HEAD. Never force. If preview moved while this run was in progress, merge
// it in when that is clean; a conflict is reported (never resolved by guessing) and the work
// stays safe on the run branch.
//
// The Actions checkout only has the work branch. `git fetch origin preview` then updates
// FETCH_HEAD and leaves `origin/preview` absent, so a later merge was reported as
// preview_diverged even when the histories merged cleanly and the deploy never started.
async function promoteToPreview(payload) {
  const target = String(payload.branch || "");
  if (!/^[A-Za-z0-9._/-]+$/.test(target) || target.includes("..")) {
    return { ok: false, reason: "no_target_branch" };
  }
  const tracking = `refs/remotes/origin/${target}`;
  const push = () => exec(
    "git", ["push", "origin", `HEAD:refs/heads/${target}`], { timeoutMs: 180000, quiet: true },
  );
  let mergedAny = false;
  for (let attempt = 1; attempt <= 3; attempt += 1) {
    const result = await push();
    if (result.code === 0) return { ok: true, merged: mergedAny };
    const rejected = /non-fast-forward|fetch first|\[rejected\]/i.test(`${result.stderr}\n${result.stdout}`);
    if (!rejected) return { ok: false, reason: "push_rejected", detail: String(result.stderr || "").slice(0, 300) };
    const fetched = await exec(
      "git", ["fetch", "origin", `+refs/heads/${target}:${tracking}`],
      { timeoutMs: 120000, quiet: true },
    );
    if (fetched.code !== 0) {
      return { ok: false, reason: "fetch_failed", detail: String(fetched.stderr || "").slice(0, 300) };
    }
    const merged = await exec(
      "git",
      ["merge", "--no-edit", "-m", `p2hs: merge ${target} into run ${String(payload.run_id).slice(0, 8)}`, tracking],
      { quiet: true },
    );
    if (merged.code !== 0) {
      await exec("git", ["merge", "--abort"], { quiet: true });
      return { ok: false, reason: "preview_diverged", detail: String(merged.stderr || merged.stdout || "").slice(0, 300) };
    }
    mergedAny = true;
  }
  return { ok: false, reason: "push_rejected" };
}

async function callback(payload, body) {
  const tokens = callbackAuthTokens(payload);
  if (!payload.callback_url || !tokens.length) return;
  let lastError = null;
  for (const token of tokens) {
    let authFailure = false;
    for (let attempt = 1; attempt <= CALLBACK_ATTEMPTS; attempt += 1) {
      try {
        const response = await fetch(payload.callback_url, {
          method: "POST",
          headers: {
            "Authorization": `Bearer ${token}`,
            "Content-Type": "application/json",
          },
          // Every report carries where the run branch checkpoint is (empty without a work branch).
          body: JSON.stringify({ ...workFields(), ...body }),
          signal: AbortSignal.timeout(30000),
        });
        if (response.ok) return;
        lastError = new Error(`P2HS completion callback returned HTTP ${response.status}`);
        authFailure = response.status === 401 || response.status === 403;
      } catch (error) {
        lastError = error;
        authFailure = false;
      }
      if (attempt < CALLBACK_ATTEMPTS) await sleep(1000 * attempt);
    }
    // The run-scoped grant can be rejected (e.g. expired) even though the run is
    // legitimate -- try the legacy shared secret before treating this run as
    // unreportable.
    if (!authFailure) break;
  }
  throw lastError || new Error("P2HS completion callback failed");
}

function provisionPayload(manifest) {
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return null;
  const payload = {
    version: manifest.version ?? 1,
    entities: Array.isArray(manifest.entities) ? manifest.entities : [],
    storage_buckets: Array.isArray(manifest.storage_buckets) ? manifest.storage_buckets : [],
  };
  if (!payload.entities.length && !payload.storage_buckets.length) return null;
  return payload;
}

// p2hs/feature-packs.json's install/compose arrays -- see
// src/domain/feature_packs/requirements_spec.py (compose) and
// feature_packs_manifest.py (install) for what each entry shape means. Extraction
// only: per-entry validation happens Core-side, same non-blocking-on-malformed-entry
// posture as feature_packs_manifest.py's own loader.
function packsPayload(manifest) {
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return null;
  const pack_installs = Array.isArray(manifest.install) ? manifest.install : [];
  const pack_compositions = Array.isArray(manifest.compose) ? manifest.compose : [];
  if (!pack_installs.length && !pack_compositions.length) return null;
  return { pack_installs, pack_compositions };
}

/**
 * Pre-push gate: resolves p2hs/provision.json AND p2hs/feature-packs.json against
 * Core, before this run's own push -- see resolve_codegen_capabilities in
 * workers/tasks/codegen.py. Returns { proceed, pack_install_outcomes,
 * composition_outcomes } -- `proceed: false` means already reported via callback()
 * and the caller must not push. Distinguishes three outcomes:
 * - "ready": proceed. Also carries the resolved pack outcomes (including any
 *   *optional* gaps, which don't block the push) so the caller can fold them into
 *   resultBody -- otherwise an optional gap silently never reaches the final run
 *   status/message for this execution path.
 * - "blocked": a REQUIRED capability gap -- not an error. Reported as status
 *   "blocked" (never as a thrown/CI failure -- an honest, non-error outcome).
 * - deadline exhausted without ever resolving: connectivity/availability, NOT a
 *   capability gap. Reported as status "failed" (retriable via the dashboard --
 *   see CodegenService.retry_run) with a message that says so explicitly, instead
 *   of a raw crash indistinguishable from a real block. Network-level fetch
 *   failures are treated identically to a non-ok HTTP response throughout the
 *   loop -- both just mean "not yet resolved," only the deadline distinguishes
 *   "still trying" from "give up and report."
 */
async function resolveCapabilitiesBeforePush(payload, provisionManifest, packsManifest) {
  const provisionDeclaration = provisionPayload(provisionManifest);
  const packsDeclaration = packsPayload(packsManifest);
  if (!provisionDeclaration && !packsDeclaration) {
    return { proceed: true, pack_install_outcomes: [], composition_outcomes: [] };
  }
  const tokens = callbackAuthTokens(payload);
  if (!payload.callback_url || !tokens.length) throw new Error("Core capability-resolution callback is required");
  const url = payload.callback_url.replace(/\/complete$/, "/provision");
  const body = {
    provision: provisionDeclaration || {},
    pack_installs: packsDeclaration ? packsDeclaration.pack_installs : [],
    pack_compositions: packsDeclaration ? packsDeclaration.pack_compositions : [],
  };
  let tokenIndex = 0;
  const deadline = Date.now() + 300000;
  while (Date.now() < deadline) {
    let result = null;
    let response = null;
    try {
      response = await fetch(url, {
        method: "POST",
        headers: { "Authorization": `Bearer ${tokens[tokenIndex]}`, "Content-Type": "application/json" },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(30000),
      });
    } catch {
      // Network-level failure -- fall through and retry, same as a non-ok response.
    }
    if (response && (response.status === 401 || response.status === 403)) {
      // The run-scoped grant can be rejected (e.g. expired) even though the run
      // is legitimate -- try the legacy shared secret before failing fast. Only
      // once every token has been rejected is this a real auth misconfiguration,
      // deterministic and not worth burning the deadline on.
      if (tokenIndex < tokens.length - 1) {
        tokenIndex += 1;
      } else {
        throw new Error(
          `Core capability callback authorization failed (HTTP ${response.status}). ` +
          "Verify CODEGEN_ACTIONS_CALLBACK_TOKEN in Core matches the " +
          "P2HS_CODEGEN_CALLBACK_TOKEN GitHub Actions secret."
        );
      }
    } else if (response && response.ok) {
      try {
        result = await response.json();
      } catch {
        // Malformed body -- treat like a non-ok response, retry.
      }
    }
    if (result && result.status === "ready") {
      // Core normalizes AI-written capability declarations (see manifest_repair.py) and is
      // authoritative for the canonical form; say what it changed so the run log shows it.
      for (const outcome of result.composition_outcomes || []) {
        for (const rename of outcome.renamed || []) {
          log(`capability '${outcome.name}': ${rename.path} '${rename.from}' -> '${rename.to}'`);
        }
      }
      return {
        proceed: true,
        pack_install_outcomes: result.pack_install_outcomes || [],
        composition_outcomes: result.composition_outcomes || [],
      };
    }
    if (result && result.status === "blocked") {
      const message = result.message || "A requested capability could not be applied.";
      log(`capability gate blocked: ${message}`);
      await callback(payload, {
        run_id: payload.run_id,
        status: "blocked",
        summary: message,
        failure_class: result.failure_class || "capability_gap",
        retryable: result.retryable ?? true,
        correction: result.correction,
      });
      return { proceed: false };
    }
    if (result && result.status === "failed") {
      // Core says *why* (failure_class / retryable / message -- see
      // classify_provisioning_failure). Deterministic, so fail fast rather than burn the
      // deadline, and report it through the callback like the connectivity case below, so
      // the run's error_message explains itself instead of a bare CI crash. An older Core
      // answers {status: "failed"} with no message: fall back to the old wording.
      const message = result.message || "Required platform resource provisioning failed";
      log(`capability provisioning failed [${result.failure_class || "unclassified"}, retryable=${result.retryable ?? "unknown"}]: ${message}`);
      await callback(payload, {
        run_id: payload.run_id,
        status: "failed",
        error_message: message,
        failure_class: result.failure_class,
        retryable: result.retryable,
        correction: result.correction,
      });
      return { proceed: false };
    }
    if (result && result.status !== "pending") {
      // Any other unexpected status: still deterministic, still fail fast.
      throw new Error("Required platform resource provisioning failed");
    }
    await sleep(2000);
  }
  await callback(payload, {
    run_id: payload.run_id,
    status: "failed",
    error_message: "Couldn't verify platform capabilities before pushing (connectivity). Retry from the dashboard.",
  });
  return { proceed: false };
}

const LOCAL_PLATFORM_ROUTES = [
  "server/api/admin/users.ts",
  "server/api/admin/users.js",
  "server/api/admin/users.get.ts",
  "server/api/admin/users.post.ts",
  "server/api/admin/users/index.ts",
  "server/api/admin/users/index.js",
  "server/api/admin/users/index.get.ts",
  "server/api/admin/users/index.post.ts",
  "functions/api/admin/users.js",
  "functions/api/admin/users.ts",
  "functions/api/admin/users/index.js",
  "functions/_lib/p2hs-auth-users.mjs",
  "server/utils/p2hs-auth-users.mjs",
  "functions/_lib/p2hs-auth-password.mjs",
  "server/utils/p2hs-auth-password.mjs",
  "server/api/auth/forgot-password.ts",
  "server/api/auth/forgot-password.js",
  "server/api/auth/forgot-password.post.ts",
  "server/api/auth/reset-password.ts",
  "server/api/auth/reset-password.js",
  "server/api/auth/reset-password.post.ts",
  "functions/api/auth/forgot-password.js",
  "functions/api/auth/reset-password.js",
  "functions/_lib/p2hs-records.mjs",
  "functions/api/records/[[path]].js",
  "functions/api/records.js",
  "server/utils/p2hs-records.mjs",
  "server/api/records/[...path].ts",
  "server/api/records.ts",
  "server/api/records/index.ts",
  "functions/_lib/p2hs-contact.mjs",
  "functions/_lib/p2hs-forms-runtime.mjs",
  "functions/_lib/p2hs-forms-submit.mjs",
  "functions/api/forms/[formKey]/submissions.js",
  "server/utils/p2hs-forms-runtime.mjs",
  "server/utils/p2hs-forms-submit.mjs",
  "server/api/forms/[formKey]/submissions.post.ts",
  "functions/_lib/p2hs-auth-magic.mjs",
  "functions/api/contact.js",
  "functions/api/contact/inbox.js",
  "functions/api/public-config.js",
  "functions/api/auth/magic-link/consume.js",
  "server/utils/p2hs-contact.mjs",
  "server/utils/p2hs-auth-magic.mjs",
  "server/api/contact.post.ts",
  "server/api/contact/inbox.get.ts",
  "server/api/public-config.get.ts",
  "server/api/auth/magic-link/consume.get.ts",
];
const ROLE_ALIASES = {
  super_admin: "super_admin", superadmin: "super_admin", owner: "super_admin",
  admin: "admin", staff: "admin", administrator: "admin",
  member: "member", customer: "member", user: "member", student: "member",
};
const EMAIL_RE = /[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g;

function isPlatformAdapterText(text) {
  return String(text || "").includes("p2hs-auth-users") || String(text || "").includes("p2hs-auth-password") || String(text || "").includes("p2hs-records") || String(text || "").includes("p2hs-contact") || String(text || "").includes("p2hs-auth-magic") || String(text || "").includes("p2hs-forms-submit") || String(text || "").includes("p2hs-forms-runtime");
}

function bakPath(rel) {
  return path.join(".p2hs", "local-bak", rel);
}

async function writeBak(rel, content) {
  const dest = bakPath(rel);
  await mkdir(path.dirname(dest), { recursive: true });
  await writeFile(dest, content, "utf8");
}

async function purgeScannedBackups() {
  async function walk(dir) {
    let entries = [];
    try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
    for (const entry of entries) {
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) await walk(full);
      else if (entry.name.endsWith(".p2hs-local.bak")) await rm(full, { force: true });
    }
  }
  await walk("server");
  await walk("functions");
}

async function backupLocalPlatformRoutes() {
  for (const rel of LOCAL_PLATFORM_ROUTES) {
    if (!existsSync(rel)) continue;
    try {
      const text = await readFile(rel, "utf8");
      if (isPlatformAdapterText(text)) continue;
      await writeBak(rel, text);
      await rm(rel, { force: true });
    } catch {}
  }
}

async function retireShadowStores() {
  for (const [rel, stub] of Object.entries(SHADOW_STUBS || {})) {
    if (!existsSync(rel)) continue;
    try {
      const text = await readFile(rel, "utf8");
      if (text.includes("__P2HS_RETIRED_LOCAL_STORE__")) continue;
      await writeBak(rel, text);
      await writeFile(rel, String(stub), "utf8");
    } catch {}
  }
}

async function installPlatformAdapters(payload) {
  const portLock = JSON.parse((payload.record_adapter_files || payload.platform_adapter_files || {})["p2hs/platform-ports.lock.json"] || "null");
  if (portLock) {
    const prepared = await exec(process.platform === "win32" ? "python" : "python3", ["-c", __P2HS_BACKEND_GUARD__, ".", "-", "--prepare"], { input: JSON.stringify(portLock) });
    if (prepared.code !== 0) throw new Error(prepared.stderr || "Platform route preparation failed");
  }
  await backupLocalPlatformRoutes();
  const files = payload.record_adapter_files || payload.platform_adapter_files || {};
  for (const [filePath, content] of Object.entries(files)) {
    const normalized = path.normalize(filePath);
    if (normalized.startsWith("..") || path.isAbsolute(normalized)) throw new Error("Unsafe adapter path");
    await mkdir(path.dirname(normalized), { recursive: true });
    if (existsSync(normalized)) {
      const before = await readFile(normalized, "utf8");
      if (before !== String(content)) await writeBak(normalized, before);
    }
    await writeFile(normalized, String(content), "utf8");
  }
  await retireShadowStores();
  await purgeScannedBackups();
  const migrated = await exec(process.platform === "win32" ? "python" : "python3", ["-c", __P2HS_BACKEND_GUARD__, ".", "--migrate"]);
  if (migrated.code !== 0) throw new Error(migrated.stderr || "Legacy path migration failed");
}

// A provider can still overwrite a locked adapter after reading the agent rules. The
// platform baseline is the authoritative copy for this attempt, so restore locked files
// from that commit before the final contract check. This also covers an older payload
// whose adapter-file map omitted a newly locked path; the baseline was created from the
// current payload before the provider ran.
async function restoreLockedPortArtifacts(baseHead, payload) {
  let lock = null;
  const files = payload.record_adapter_files || payload.platform_adapter_files || {};
  try {
    lock = JSON.parse(String(files["p2hs/platform-ports.lock.json"] || "null"));
  } catch {}
  if (!lock || typeof lock !== "object" || !lock.files || typeof lock.files !== "object") {
    const baseline = await exec("git", ["show", `${baseHead}:p2hs/platform-ports.lock.json`], { quiet: true });
    if (baseline.code !== 0) return;
    try { lock = JSON.parse(baseline.stdout); } catch { return; }
  }
  for (const relative of Object.keys(lock.files || {})) {
    const normalized = path.normalize(String(relative));
    if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) {
      throw new Error(`Unsafe platform artifact path: ${relative}`);
    }
    // The signed dispatch payload matches the lock used for this attempt and is
    // therefore authoritative. The baseline is only a compatibility fallback for
    // an older payload that omitted a newly locked path.
    const restored = await exec("git", ["show", `${baseHead}:${normalized}`], { quiet: true });
    const content = files[relative] !== undefined
      ? files[relative]
      : restored.code === 0
        ? restored.stdout
        : undefined;
    if (content === undefined) {
      throw new Error(`Platform port artifact missing from baseline and payload: ${relative}`);
    }
    await mkdir(path.dirname(normalized), { recursive: true });
    await writeFile(normalized, String(content), "utf8");
  }
}

function inferLegacyRole(text) {
  const match = String(text || "").match(/role["']?\s*[:=]\s*["'](super_admin|superadmin|admin|staff|owner|member|customer|user|student)["']/i);
  const key = String(match?.[1] || "member").toLowerCase().replace(/[-\s]/g, "_");
  return ROLE_ALIASES[key] || "member";
}

async function walkCodeFiles(dir, out = []) {
  let entries = [];
  try { entries = await readdir(dir, { withFileTypes: true }); } catch { return out; }
  for (const entry of entries) {
    if ([".git", "node_modules", ".nuxt", ".output", "dist", "coverage"].includes(entry.name)) continue;
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) await walkCodeFiles(full, out);
    else if (/\.(ts|js|mjs|cjs|vue|json)(?:\.p2hs-local\.bak)?$/i.test(entry.name)) out.push(full);
  }
  return out;
}

async function scanLegacyUsers() {
  const found = new Map();
  const files = await walkCodeFiles(".");
  for (const file of files.slice(0, 4000)) {
    let text = "";
    try { text = await readFile(file, "utf8"); } catch { continue; }
    if (isPlatformAdapterText(text)) continue;
    for (const email of text.match(EMAIL_RE) || []) {
      const lower = email.toLowerCase();
      if (lower.endsWith("@p2hs.com") || lower.endsWith("@example.com")) continue;
      const idx = text.toLowerCase().indexOf(lower);
      const window = text.slice(Math.max(0, idx - 400), idx + 400);
      if (!found.has(lower)) found.set(lower, { email: lower, role: inferLegacyRole(window), name: "" });
    }
  }
  return [...found.values()];
}

async function cloudCutover(payload, users) {
  if (!payload.callback_url || !callbackAuthTokens(payload).length) return;
  const url = String(payload.callback_url).replace(/\/complete$/, "/cloud-cutover");
  let lastStatus = null;
  let lastError = null;
  for (const token of callbackAuthTokens(payload)) {
    try {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ users: users || [] }),
        signal: AbortSignal.timeout(30000),
      });
      if (response.ok) return;
      lastStatus = response.status;
      // A mixed-version rollout may not know the new run grant yet. Try the
      // legacy shared secret before treating this optional step as skipped.
      if (response.status !== 401 && response.status !== 403) break;
    } catch (error) {
      lastError = error;
    }
  }
  if (lastStatus !== null) log(`cloud cutover skipped: HTTP ${lastStatus}`);
  else if (lastError) log(`cloud cutover skipped: ${String(lastError?.message || lastError).slice(0, 200)}`);
}

// feature-packs.json is a Core-owned control-plane manifest: every fresh attempt starts from
// Core's projection alone, so a stale manifest cannot be inherited. The one exception is an
// attempt that CONTINUES the same run on its work branch (a resume, a correction or a time-budget
// continuation): the manifest on the branch is then that run's own agent output, which must be
// validated or corrected, not thrown away. Core's required installs are still added if missing.
async function writeCoreFeaturePacks(payload, required) {
  const continuing = Boolean(workBranchOf(payload)
    && (payload.resume || Number(payload.continuation_attempt || 0) > 0));
  if (continuing && existsSync("p2hs/feature-packs.json")) {
    try {
      const kept = JSON.parse(await readFile("p2hs/feature-packs.json", "utf8"));
      if (kept && typeof kept === "object" && !Array.isArray(kept)) {
        const installs = Array.isArray(kept.install) ? kept.install : [];
        const have = new Set(installs.map((row) => String(row?.pack_slug || "")));
        for (const row of (required.install || [])) {
          if (row?.pack_slug && !have.has(String(row.pack_slug))) installs.push(row);
        }
        kept.version = 1;
        kept.install = installs;
        await writeFile("p2hs/feature-packs.json", JSON.stringify(kept, null, 2) + "\n", "utf8");
        return "kept";
      }
    } catch {}
  }
  await writeFile("p2hs/feature-packs.json", JSON.stringify(required, null, 2) + "\n", "utf8");
  return "replaced";
}

async function materializeCoreComposition(payload) {
  const rendered = payload?.context?.core_composition_files;
  if (rendered && typeof rendered === "object" && !Array.isArray(rendered)) {
    await mkdir("p2hs", { recursive: true });
    for (const [relative, content] of Object.entries(rendered)) {
      if (relative === "p2hs/feature-packs.json") continue;
      await writeFile(relative, String(content), "utf8");
    }
    const required = JSON.parse(String(rendered["p2hs/feature-packs.json"] || "{}"));
    // This is a Core-owned control-plane projection.  Never merge the
    // provider's previous manifest into it; stale installs and compose entries
    // must not survive a retry or bypass the GenerationPlan.
    await writeCoreFeaturePacks(payload, required);
    await verifyCoreComposition(payload);
    log("materialized canonical Core composition projections");
    return;
  }
  const plan = payload?.context?.generation_plan;
  const composition = plan?.composition;
  if (!composition || typeof composition !== "object") return;
  const blockIds = [...new Set((composition.app_block_ids || []).map(String))].sort();
  const packIds = [...new Set((composition.feature_pack_ids || []).map(String))].sort();
  const primitiveIds = [...new Set((composition.primitive_ids || []).map(String))].sort();
  const cloudIds = [...new Set((composition.cloud_capability_ids || []).map(String))].sort();
  const formDefinitions = composition.form_definitions && typeof composition.form_definitions === "object"
    ? composition.form_definitions : {};
  const blocks = blockIds.map((id) => ({
    id,
    version: "1.0.0",
    reason: "Required by Core GenerationPlan.",
    slots: {},
    dependencies: [],
    protected_bindings: [],
  }));
  await mkdir("p2hs", { recursive: true });
  await writeFile("p2hs/app-blocks-plan.json", JSON.stringify({
    schema_version: "1",
    requested_capabilities: [...new Set([...blockIds, ...cloudIds])].sort(),
    blocks,
    required_feature_packs: packIds,
    required_primitives: primitiveIds,
    runtime_dependencies: (plan.runtime?.runtime_dependencies || []).map(String).sort(),
    starter_contracts: (plan.runtime?.starter_contract_ids || []).map(String).sort(),
    adoption: "authoritative",
    feature_pack_ids: packIds,
  }, null, 2) + "\n", "utf8");
  const packs = {
    version: 1,
    install: packIds.map((pack_slug) => ({
      pack_slug,
      config: pack_slug === "contact-capture" && Object.keys(formDefinitions).length
        ? { form_definitions: formDefinitions }
        : {},
    })),
  };
  await writeCoreFeaturePacks(payload, packs);
  await verifyCoreComposition(payload);
  log(`materialized Core composition blocks=${blockIds.length} packs=${packIds.length}`);
}

async function verifyCoreComposition(payload) {
  const expected = payload?.context?.generation_plan?.composition;
  if (!expected || typeof expected !== "object") return null;
  let appPlan = {};
  let packs = {};
  try { appPlan = JSON.parse(await readFile("p2hs/app-blocks-plan.json", "utf8")); }
  catch { throw new Error("P2HS_COMPOSITION_MISMATCH:app-blocks-plan_missing"); }
  try { packs = JSON.parse(await readFile("p2hs/feature-packs.json", "utf8")); }
  catch { throw new Error("P2HS_COMPOSITION_MISMATCH:feature-packs_missing"); }
  const actualBlocks = new Set((appPlan.blocks || []).map((row) => String(row?.id || "")).filter(Boolean));
  const actualPacks = new Set((packs.install || []).map((row) => String(row?.pack_slug || "")).filter(Boolean));
  const missingBlocks = [...new Set((expected.app_block_ids || []).map(String))].filter((id) => !actualBlocks.has(id)).sort();
  const missingPacks = [...new Set((expected.feature_pack_ids || []).map(String))].filter((id) => !actualPacks.has(id)).sort();
  if (missingBlocks.length || missingPacks.length) {
    throw new Error(`P2HS_COMPOSITION_MISMATCH:blocks=${missingBlocks.join(",")};packs=${missingPacks.join(",")}`);
  }
  return {
    app_block_ids: [...actualBlocks].sort(),
    feature_pack_ids: [...actualPacks].sort(),
  };
}

async function main() {
  log(`loading payload ${payloadPath}`);
  const envelope = JSON.parse(await readFile(payloadPath, "utf8"));
  const payload = envelope.payload;
  activePayload = payload;
  const secret = process.env.P2HS_CODEGEN_PAYLOAD_SECRET || "";
  if (secret) {
    const sig = "sha256=" + createHmac("sha256", secret).update(stableStringify(payload)).digest("hex");
    if (sig !== envelope.signature) {
      throw new Error("P2HS payload signature mismatch");
    }
  }

  const gitName = payload.git_committer_name || "P2HS";
  if (process.argv.includes("--report-deployment")) {
    const saved = JSON.parse(await readFile(".p2hs/action-result.json", "utf8"));
    const head = (await exec("git", ["rev-parse", "HEAD"])).stdout.trim();
    if (saved.run_id !== payload.run_id || saved.commit_sha !== head) throw new Error("Deployment result identity mismatch");
    const succeeded = process.env.P2HS_DEPLOYMENT_STATUS === "success";
    await callback(payload, {
      ...saved, status: succeeded ? "completed" : "failed",
      error_message: succeeded ? null : "Code was saved, but the preview build or deployment failed.",
    });
    return;
  }
  const gitEmail = payload.git_committer_email || "noreply@p2hs.com";
  await exec("git", ["config", "user.name", gitName]);
  await exec("git", ["config", "user.email", gitEmail]);
  if (payload.git_push_token) {
    // actions/checkout's default GITHUB_TOKEN authenticates fetch/push for this job, but
    // GitHub never fires other workflows' `push` triggers for pushes made with it. Swap in
    // a repo-scoped installation token via the same credential slot checkout already used,
    // so the generated commit's push to preview/main starts the deploy workflow normally.
    const basic = Buffer.from(`x-access-token:${payload.git_push_token}`).toString("base64");
    await exec("git", ["config", "--local", "http.https://github.com/.extraheader", `AUTHORIZATION: basic ${basic}`], { quiet: true });
  }
  await installCursorCliConfig(gitName, gitEmail);
  await installP2hsGitHook();

  let leftoverUsers = await scanLegacyUsers();
  await cloudCutover(payload, leftoverUsers);
  await installPlatformAdapters(payload);
  await materializeCoreComposition(payload);

  // Establish a clean, immutable platform baseline before the provider runs.
  // Without this checkpoint, adapter/runtime-contract updates become
  // indistinguishable from the app delta in the agent's commit.
  const platformStatus = await exec("git", ["status", "--porcelain"]);
  if (platformStatus.stdout.trim()) {
    const platformCommit = await exec("git", ["add", "--all"]);
    if (platformCommit.code !== 0) throw new Error("P2HS_CODEGEN_PREFLIGHT: platform baseline staging failed");
    const baselineCommit = await exec("git", ["commit", "-m", "chore: sync P2HS platform contract"]);
    if (baselineCommit.code !== 0) throw new Error("P2HS_CODEGEN_PREFLIGHT: platform baseline commit failed");
  }
  const baselineStatus = await exec("git", ["status", "--porcelain"]);
  if (baselineStatus.stdout.trim()) throw new Error("P2HS_CODEGEN_PREFLIGHT: codegen base checkpoint is dirty");

  const combinedPrompt = `${payload.system_prompt || ""}\n\n${payload.user_prompt || ""}`.trim();
  await writeFile(".cursorrules", payload.system_prompt || "", "utf8");
  await mkdir(".p2hs/ai-context", { recursive: true });
  await writeFile(".p2hs/ai-context/system-prompt.md", payload.system_prompt || "", "utf8");
  const agentPromptPath = ".p2hs/ai-context/agent-prompt.md";
  await writeFile(agentPromptPath, combinedPrompt, "utf8");
  // Keep the spawn argv tiny. A full system+user prompt on argv hits Linux ARG_MAX (E2BIG).
  const agentPromptPointer = `Read ${agentPromptPath} in full. That file is the complete task prompt for this run. Follow it.`;
  await writeFile(".p2hs/ai-context/last-run.json", JSON.stringify({
    run_id: payload.run_id,
    provider: payload.provider,
    model: payload.cursor_model || payload.openai_model || payload.claude_model || "",
    created_at: payload.created_at,
    continuation_attempt: payload.continuation_attempt || 0,
  }, null, 2), "utf8");
  const baseHead = (await exec("git", ["rev-parse", "HEAD"])).stdout.trim();
  // Work-branch mode. `baseHead` stays this attempt's starting point (commit authorship is
  // only rewritten for commits made now: earlier ones are already pushed and never rewritten);
  // `changesBase` is where the run's changes are measured from, the preview head the branch was
  // created from, so a resumed run still sees everything it has changed.
  const workBranch = workBranchOf(payload);
  let changesBase = baseHead;
  if (workBranch) {
    activeWork = { branch: workBranch, checkpoint: null, sha: null };
    const wanted = String(payload.work_base_sha || "");
    if (SHA_RE.test(wanted)) {
      const ancestor = await exec("git", ["merge-base", "--is-ancestor", wanted, "HEAD"], { quiet: true });
      if (ancestor.code === 0) changesBase = wanted;
      else log(`work base ${wanted.slice(0, 12)} is not an ancestor of HEAD; measuring changes from this attempt only`);
    }
    activeWork.changesBase = changesBase;
  }
  // A retry after a failure past the agent only needs validation and promotion redone.
  const skipAgent = Boolean(workBranch && payload.resume && payload.resume.skip_agent === true);
  const elapsedSeconds = process.env.P2HS_JOB_STARTED_AT
    ? Math.max(0, Date.now() / 1000 - Number(process.env.P2HS_JOB_STARTED_AT)) : 0;
  // Leave ten minutes for resource provisioning, saving, push, and continuation dispatch.
  const remainingAgentSeconds = 1800 - elapsedSeconds - 600;
  if (remainingAgentSeconds < 60) throw new Error("Insufficient job time to safely start codegen");
  const requestedBudget = Number(payload.agent_budget_seconds || process.env.CURSOR_TIMEOUT || 1320);
  const agentBudgetMs = Math.max(60, Math.min(Number.isFinite(requestedBudget) ? requestedBudget : 1200, remainingAgentSeconds)) * 1000;

  let transcript = "";
  let timedOut = false;
  const provider = String(payload.provider || "").toLowerCase();
  const selectedModel = provider === "cursor"
    ? payload.cursor_model
    : provider === "claude"
      ? payload.claude_model
      : payload.openai_model;
  log(`provider=${provider} branch=${payload.branch || ""} model=${selectedModel || ""} attempt=${payload.continuation_attempt || 0} budget_ms=${agentBudgetMs}`);
  if (skipAgent) {
    transcript = "Resumed from the saved work branch; the agent was not re-run.";
    log("resuming from the run work branch: skipping the agent, re-running capability validation and promotion");
  } else if (provider === "cursor") {
    process.env.CURSOR_MODEL = payload.cursor_model || process.env.CURSOR_MODEL || "";
    const agent = process.env.CURSOR_CLI_PATH || "agent";
    const args = ["--workspace", process.cwd(), "--print", "--force", "--trust"];
    if (process.env.CURSOR_MODEL) args.push("--model", process.env.CURSOR_MODEL);
    if (payload.cursor_stream_progress) args.push("--output-format", "stream-json", "--stream-partial-output");
    if (payload.cursor_resume_session_id) {
      args.push("--resume", String(payload.cursor_resume_session_id));
      log(`resuming Cursor session ${String(payload.cursor_resume_session_id).slice(0, 12)}`);
    }
    args.push(agentPromptPointer);
    log(`running Cursor agent (${agent}) prompt_file=${agentPromptPath} prompt_chars=${combinedPrompt.length}`);
    const res = await exec(agent, args, {
      env: { CURSOR_API_KEY: process.env.CURSOR_API_KEY || "" },
      detailFormat: payload.cursor_stream_progress ? "stream-json" : null,
      timeoutMs: agentBudgetMs,
    });
    transcript = `${res.stdout}\n${res.stderr}`.trim();
    if (res.code === 124) {
      timedOut = true;
      log("Cursor agent hit the save-and-restart budget; committing progress");
    } else if (res.code !== 0) {
      throw new Error(`Cursor exited with ${res.code}\n${transcript.slice(-4000)}`);
    }
  } else if (provider === "codex") {
    process.env.OPENAI_MODEL = payload.openai_model || process.env.OPENAI_MODEL || "gpt-4o";
    if (!process.env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY is not configured");
    const codex = process.env.CODEX_CLI_PATH || "codex";
    // GitHub-hosted runners already isolate this job. Avoid Codex's bubblewrap
    // workspace-write sandbox because its network namespace setup is blocked
    // in the runner and fails with RTM_NEWADDR/Operation not permitted.
    const args = ["exec", "--sandbox", "danger-full-access", "--skip-git-repo-check"];
    if (process.env.OPENAI_MODEL) args.push("--model", process.env.OPENAI_MODEL);
    args.push(agentPromptPointer);
    log(`running OpenAI Codex CLI (${codex}) prompt_file=${agentPromptPath} prompt_chars=${combinedPrompt.length}`);
    const res = await exec(codex, args, {
      env: {
        CODEX_API_KEY: process.env.OPENAI_API_KEY || "",
        OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
      },
      input: "",
      timeoutMs: agentBudgetMs,
    });
    transcript = `${res.stdout}\n${res.stderr}`.trim();
    if (res.code === 124) {
      timedOut = true;
      log("OpenAI Codex CLI hit the save-and-restart budget; committing progress");
    } else if (res.code !== 0) {
      throw new Error(`OpenAI Codex CLI exited with ${res.code}\n${transcript.slice(-4000)}`);
    }
  } else if (provider === "claude") {
    process.env.CLAUDE_MODEL = payload.claude_model || process.env.CLAUDE_MODEL || "claude-sonnet-4-20250514";
    if (!process.env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY is not configured");
    const claude = process.env.CLAUDE_CLI_PATH || "claude";
    const args = ["--dir", process.cwd(), "--message", agentPromptPointer, "--yes"];
    if (process.env.CLAUDE_MODEL) args.push("--model", process.env.CLAUDE_MODEL);
    log(`running Claude Code CLI (${claude}) prompt_file=${agentPromptPath} prompt_chars=${combinedPrompt.length}`);
    const res = await exec(claude, args, {
      env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "" },
      input: "",
      timeoutMs: agentBudgetMs,
    });
    transcript = `${res.stdout}\n${res.stderr}`.trim();
    if (res.code === 124) {
      timedOut = true;
      log("Claude Code CLI hit the save-and-restart budget; committing progress");
    } else if (res.code !== 0) {
      throw new Error(`Claude Code CLI exited with ${res.code}\n${transcript.slice(-4000)}`);
    }
  } else if (provider === "openai") {
    process.env.OPENAI_MODEL = payload.openai_model || process.env.OPENAI_MODEL || "gpt-4o";
    if (!process.env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY is not configured");
    const prompt = [
      payload.system_prompt || "",
      "Return strict JSON: {\"files\":{\"path\":\"complete file contents\"},\"summary\":\"...\"}.",
      "Only include files that must be created or replaced.",
      payload.user_prompt || "",
    ].join("\n\n");
    const response = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: { "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        model: process.env.OPENAI_MODEL || "gpt-4o",
        messages: [{ role: "user", content: prompt }],
        response_format: { type: "json_object" },
        temperature: 0.1,
      }),
    });
    if (!response.ok) throw new Error(`OpenAI API failed: ${response.status} ${await response.text()}`);
    const data = await response.json();
    transcript = data.choices?.[0]?.message?.content || "";
    const parsed = JSON.parse(transcript || "{}");
    const files = parsed.files || {};
    for (const [filePath, content] of Object.entries(files)) {
      const normalized = path.normalize(filePath);
      if (normalized.startsWith("..") || path.isAbsolute(normalized)) throw new Error(`Unsafe path: ${filePath}`);
      await mkdir(path.dirname(normalized), { recursive: true });
      await writeFile(normalized, String(content), "utf8");
    }
  } else {
    throw new Error(`Unsupported P2HS GitHub Actions provider: ${provider}`);
  }

  const expectedPortLock = JSON.parse((payload.record_adapter_files || payload.platform_adapter_files || {})["p2hs/platform-ports.lock.json"] || "null");
  if (expectedPortLock) {
    // The provider is allowed to edit the checkout, but platform adapters are
    // Core-owned artifacts. Re-project them after generation so provider edits
    // cannot make the run fail merely by touching a protected adapter.
    await installPlatformAdapters(payload);
    await restoreLockedPortArtifacts(baseHead, payload);
    const portCheck = await exec(process.platform === "win32" ? "python" : "python3", ["-c", __P2HS_BACKEND_GUARD__, ".", "-"], { input: JSON.stringify(expectedPortLock) });
    if (portCheck.code !== 0) throw new Error(portCheck.stderr || "Platform port contract failed");
  }
  await restoreEphemeralCodegenFiles(baseHead);
  try {
    await rewriteCommitsAuthoredByP2hs(baseHead, gitName, gitEmail);
  } catch (error) {
    log(`authorship rewrite skipped: ${String(error?.message || error).slice(0, 300)}`);
  }
  await restoreEphemeralCodegenFiles(baseHead);
  leftoverUsers = await scanLegacyUsers();
  await cloudCutover(payload, leftoverUsers);

  const status = await exec("git", ["status", "--porcelain"]);
  if (status.stdout.trim()) {
    log("committing generated changes");
    const staged = await stagePublishableAndAssert();
    if (staged.length) {
      const message = p2hsCommitMessage(
        (payload.commit_message || `p2hs: apply codegen run ${payload.run_id}`).slice(0, 200),
      );
      const commit = await exec("git", ["commit", "-m", message]);
      transcript += `\n${commit.stdout}\n${commit.stderr}`;
    }
  }

  const head = await exec("git", ["rev-parse", "HEAD"]);
  let commitSha = head.stdout.trim();
  // Measure the owner's request from the post-sync HEAD. `changesBase` still
  // includes the platform-contract commit, which is not the request.
  const madeProgress = commitSha !== changesBase;
  if (!madeProgress) {
    const err = timedOut
      ? "The GitHub Actions codegen runner timed out before creating a code change commit."
      : "The GitHub Actions codegen runner completed without creating a code change commit.";
    throw new Error(err);
  }
  const files = await exec("git", ["diff", "--name-only", `${baseHead}..HEAD`]);
  const changedFiles = files.stdout.split(/\r?\n/).map((x) => normalizeRepoPath(x)).filter(Boolean);
  const userVisibleFiles = changedFiles.filter((x) => !isInternalCodegenPath(x) && !isPlatformProjectionPath(x, payload));
  if (!userVisibleFiles.length && !timedOut) {
    throw new Error("The GitHub Actions codegen runner completed without customer-visible file changes.");
  }
  let pendingUserInputs = null;
  if (existsSync("p2hs/required-user-inputs.json")) {
    try {
      pendingUserInputs = JSON.parse(await readFile("p2hs/required-user-inputs.json", "utf8"));
    } catch {}
  }
  let l3Candidates = [];
  try {
    const l3Plan = JSON.parse(await readFile("p2hs/l3-plan.json", "utf8"));
    if (Array.isArray(l3Plan?.candidates)) l3Candidates = l3Plan.candidates;
  } catch {}

  const cursorSessionId = extractCursorSessionId(transcript) || payload.cursor_resume_session_id || null;
  const backendCheck = await exec(process.platform === "win32" ? "python" : "python3", ["-c", __P2HS_BACKEND_GUARD__, "."]);
  if (backendCheck.code !== 0) throw new Error(backendCheck.stderr || "Cloud persistence contract failed");
  // Provider execution must not remove or rewrite Core's authoritative
  // composition projections before the result is accepted.
  await verifyCoreComposition(payload);
  // Parse strictly: malformed declarations must never silently become no resources.
  const pendingProvisionFile = existsSync("p2hs/provision.json")
    ? JSON.parse(await readFile("p2hs/provision.json", "utf8")) : null;
  if (existsSync("p2hs/provision.json") && (!pendingProvisionFile || typeof pendingProvisionFile !== "object" || Array.isArray(pendingProvisionFile))) throw new Error("Invalid p2hs/provision.json");
  const pendingProvision = provisionPayload(pendingProvisionFile);
  // Same non-blocking-on-malformed-entry posture as feature_packs_manifest.py's own
  // loader -- an invalid p2hs/feature-packs.json here just means no packs are
  // resolved this run, not a thrown error (unlike provision.json above).
  let pendingPacksFile = null;
  if (existsSync("p2hs/feature-packs.json")) {
    try {
      pendingPacksFile = JSON.parse(await readFile("p2hs/feature-packs.json", "utf8"));
    } catch {}
  }
  // Overlay is replace-all desired state once Core applies it. Invalid JSON here is
  // skipped locally; Core validates known keys + protected source placeholders before
  // mutating Cloud. enabled:false means ignore override / use default, not skip send.
  let pendingEmailTemplates = null;
  if (existsSync("p2hs/email-templates.json")) {
    try {
      pendingEmailTemplates = JSON.parse(await readFile("p2hs/email-templates.json", "utf8"));
    } catch {}
  }
  const resultBody = {
    run_id: payload.run_id,
    status: timedOut ? "needs_continuation" : "completed",
    // Bill only the runner execution interval, not time waiting in GitHub's queue.
    execution_seconds: Math.max(0, Math.floor(Date.now() / 1000) - Number(process.env.P2HS_JOB_STARTED_AT || Math.floor(Date.now() / 1000))),
    provider,
    commit_sha: commitSha,
    branch: payload.branch,
    summary: transcript.split("\n").filter(Boolean).slice(-20).join("\n").slice(0, 4000),
    transcript: transcript.slice(-60000),
    files_changed: userVisibleFiles,
    pending_user_inputs: pendingUserInputs,
    l3_candidates: l3Candidates,
    l3_candidate_ids: l3Candidates.map((item) => String(item.candidate_id || "")).filter(Boolean),
    pending_provision: pendingProvision,
    pending_email_templates: pendingEmailTemplates,
    legacy_users: leftoverUsers,
    cursor_session_id: cursorSessionId,
    continuation_attempt: Number(payload.continuation_attempt || 0) + (timedOut ? 1 : 0),
  };

  if (String(payload.result_delivery || "") === "core_bundle_upload") {
    await mkdir(".p2hs", { recursive: true });
    await writeFile(".p2hs/local-result.json", JSON.stringify(resultBody, null, 2), "utf8");
    log(`prepared local result ${commitSha.slice(0, 12)} for Core upload`);
    await writeGithubOutput({ commitSha, continued: timedOut, deployRequired: false });
    await Promise.allSettled(progressSends.splice(0));
    return;
  }

  if (workBranch && timedOut) {
    // Save-and-restart: keep progress on the run branch only. Preview is not advanced and the
    // gate waits for the attempt that actually finishes.
    const saved = await pushCheckpoint(payload, workBranch, "time budget");
    if (!saved) throw new Error("Progress could not be saved to the run branch");
    commitSha = saved;
    resultBody.commit_sha = saved;
    activeWork.checkpoint = "agent_output";
    activeWork.sha = saved;
    await callback(payload, { ...resultBody });
    await writeGithubOutput({ commitSha, continued: true, deployRequired: false, deployBranch: payload.branch });
    await Promise.allSettled(progressSends.splice(0));
    return;
  }
  if (workBranch) {
    const saved = await pushCheckpoint(payload, workBranch, "agent output");
    if (saved) {
      commitSha = saved;
      resultBody.commit_sha = saved;
      activeWork.checkpoint = "agent_output";
      activeWork.sha = saved;
    } else {
      log("could not save a checkpoint to the run branch; continuing without one");
    }
  }
  // Even continuation pushes can trigger the standalone deploy workflow.
  const capabilities = await resolveCapabilitiesBeforePush(payload, pendingProvisionFile, pendingPacksFile);
  if (!capabilities.proceed) {
    log("skipping push and in-workflow deploy: capabilities not ready");
    await writeGithubOutput({ commitSha, continued: false, deployRequired: false });
    await Promise.allSettled(progressSends.splice(0));
    return;
  }
  // Only optional gaps can reach here (a required one already returned above) --
  // still worth carrying through so the final run status/message reflects them
  // (see CodegenService.report_run_complete / run_codegen's resume_finalize).
  resultBody.pending_pack_install_outcomes = capabilities.pack_install_outcomes;
  resultBody.pending_pack_composition_outcomes = capabilities.composition_outcomes;
  if (workBranch) {
    if (activeWork.sha) activeWork.checkpoint = "capabilities_provisioned";
    log(`promoting the validated run branch to ${payload.branch}`);
    const promoted = await promoteToPreview(payload);
    if (!promoted.ok) {
      const message = promoted.reason === "preview_diverged"
        ? "Preview changed while this run was in progress and could not be merged automatically. Your work is saved on the run branch and nothing on preview was changed."
        : "The validated changes could not be promoted to preview. Your work is saved on the run branch and nothing on preview was changed.";
      log(`promotion failed: ${promoted.reason}${promoted.detail ? ` (${String(promoted.detail).replace(/\s+/g, " ").trim()})` : ""}`);
      await callback(payload, { run_id: payload.run_id, status: "failed", error_message: message, failure_class: promoted.reason, retryable: true });
      await writeGithubOutput({ commitSha, continued: false, deployRequired: false });
      await Promise.allSettled(progressSends.splice(0));
      return;
    }
    if (promoted.merged) {
      commitSha = (await exec("git", ["rev-parse", "HEAD"], { quiet: true })).stdout.trim();
      resultBody.commit_sha = commitSha;
      // keep the run branch in step with what was promoted (best effort, plain push)
      await exec("git", ["push", "origin", `HEAD:refs/heads/${workBranch}`], { timeoutMs: 180000, quiet: true });
    }
    activeWork.checkpoint = "promoted";
    activeWork.sha = commitSha;
    log(`Changes have been promoted to ${payload.branch}`);
  } else {
    log("Pushing updated changes to preview");
    const pushed = await exec(
      "git", ["push", "origin", `HEAD:${payload.branch}`],
      { timeoutMs: 180000, quiet: true },
    );
    if (pushed.code !== 0) throw new Error("Generated commit could not be pushed");
    log("Changes have been saved to preview");
  }

  if (timedOut) {
    log("requesting a new GitHub Actions codegen process for the same run");
  }
  const deployRequired = !timedOut && Boolean(payload.deployment_required);
  if (deployRequired) {
    await saveDeployArtifact(resultBody);
    await callback(payload, { ...resultBody, status: "deploying" });
  } else {
    log("reporting completed result to Core");
    await callback(payload, { ...resultBody });
  }
  await writeGithubOutput({ commitSha, continued: timedOut, deployRequired, deployBranch: workBranch ? payload.branch : "" });
  await Promise.allSettled(progressSends.splice(0));
}

main().catch(async (error) => {
  try {
    const envelope = JSON.parse(await readFile(payloadPath, "utf8"));
    await saveWorkOnFailure(envelope.payload || {});
    const message = String(error?.message || error);
    const authFailure = message.includes("Generated auth conformance failed:");
    const authErrors = authFailure
      ? [...message.matchAll(/(AUTH-GEN-\d+)\s+([^:\n]+):(\d+):\s*([^\n]+)/g)].slice(0, 20).map((match) => ({
          path: `${match[2]}:${match[3]}`,
          code: match[1],
          message: match[4],
        }))
      : [];
    // Auth source violations are deterministic, agent-correctable failures. Once the
    // recovery commit is on the run branch, hand the exact findings back through the
    // existing bounded correction path instead of making the owner rediscover the issue.
    if (authFailure && activeWork?.sha) activeWork.checkpoint = "agent_output";
    await callback(envelope.payload || {}, {
      status: "failed",
      ...(authFailure ? {
        status: "blocked",
        failure_class: "invalid_requirement",
        retryable: true,
        correction: { entries: [{ name: "generated-auth-conformance", errors: authErrors }] },
      } : {}),
      error_message: message.slice(0, 4000),
      transcript: String(error?.stack || error).slice(-60000),
    });
  } catch (callbackError) {
    console.error(callbackError);
  }
  console.error(error);
  await Promise.allSettled(progressSends.splice(0));
  process.exit(1);
});
""".replace("__P2HS_BACKEND_GUARD__", json.dumps(guard)).replace(
        "__P2HS_SHADOW_STUBS__", json.dumps(shadow_store_stubs())
    )


async def _ensure_work_branch(
    github, repo_full_name: str, run_id: UUID, preview_branch: str,
    plan_context: Optional[dict[str, Any]], settings,
) -> Optional[dict[str, Any]]:
    """Create (once) or reuse the run's work branch. Returns its state, or None when the
    feature is off (legacy behaviour).

    Never force-pushes and never recreates an existing branch: a retry, a continuation and a
    correction all reuse the same branch and simply add commits to it. With the feature on this
    fails closed: a branch Core has no trustworthy record of raises instead of falling back to
    writing on preview."""
    if not getattr(settings, "codegen_work_branch_enabled", False):
        return None
    from src.domain.codegen.work_branch import new_state, valid_state, work_branch_name

    branch = work_branch_name(run_id)
    stored = (plan_context or {}).get("work_branch")
    head = await github.get_ref_head_sha(repo_full_name, branch)
    if head is None:
        base_sha = await github.get_ref_head_sha(repo_full_name, preview_branch)
        if not base_sha:
            raise RuntimeError(f"Preview branch {preview_branch!r} has no HEAD commit")
        await github.create_branch_from_sha(repo_full_name, branch, base_sha)
        return new_state(run_id, preview_branch, base_sha)
    if valid_state(stored, run_id):
        return dict(stored)
    # The branch exists but Core has no trustworthy record of its base: do not guess, and do not
    # fall back to committing on preview either.
    logger.warning(
        "work_branch_state_missing", run_id=str(run_id), branch=branch, repo=repo_full_name
    )
    raise RuntimeError(
        f"Work branch {branch} exists but its saved state is missing; not dispatching "
        "(preview was not touched). Delete the branch to start the run clean."
    )


async def _static_conversion_for_dispatch(
    *, settings, github, repo_full_name: str, branch: str, workspace_id: UUID, project_id: UUID,
    project_type: str, plan_context: Optional[dict[str, Any]],
) -> tuple[dict[str, Any], str, Optional[dict[str, Any]]]:
    """Files to commit, prompt appendix and a summary for a migrated/replaced static prototype.

    Deterministic HTML -> Nuxt conversion (static_conversion.py). Off unless enabled and the request is a
    migrate/replace of a Nuxt project with imported source. Never raises: any problem means no conversion and
    the run proceeds exactly as before."""
    from src.domain.codegen import static_conversion

    if not static_conversion.is_eligible(settings, project_type, plan_context):
        return {}, "", None
    try:
        from src.domain.codegen.import_snapshot import load_text_files

        site = await load_text_files(settings, workspace_id, project_id, plan_context)
        if not site:
            return {}, "", None
        try:
            existing = await github.list_tree_paths(repo_full_name, branch)
        except Exception as exc:  # deletions are an improvement, not a requirement
            logger.warning(
                "static_conversion_tree_unavailable", project_id=str(project_id), error_type=type(exc).__name__
            )
            existing = None
        conversion = static_conversion.plan_conversion(site, existing)
        if conversion is None:
            return {}, "", None
        logger.info(
            "static_conversion_planned",
            project_id=str(project_id),
            pages=[p["route"] for p in conversion.summary["pages"]],
            skipped=len(conversion.summary["skipped"]),
            deleted=len(conversion.deleted),
        )
        return conversion.files, conversion.appendix, conversion.summary
    except Exception as exc:
        logger.warning("static_conversion_failed", project_id=str(project_id), error_type=type(exc).__name__)
        return {}, "", None


async def dispatch_codegen_actions_run(
    *,
    db_factory,
    run_id: UUID,
    project_id: UUID,
    system_prompt: str,
    user_prompt: str,
    plan_context: Optional[dict[str, Any]],
    continuation: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    """Commit the runner/payload and dispatch the repo workflow."""
    from src.core.security.jwt import create_codegen_action_grant_token
    from src.domain.projects.repository import ProjectRepository
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace
    from src.workers.prompt_media_sync import prepare_prompt_media_for_actions

    settings = get_settings()
    callback_token = (settings.codegen_actions_callback_token or "").strip()
    if callback_token[:7].lower() == "bearer ":
        callback_token = callback_token[7:].strip()
    resolved_codegen = None
    if (plan_context or {}).get("codegen_configuration_source") == "database":
        from src.domain.codegen.provider_registry import CodegenConfigResolver
        async with db_factory() as config_db:
            resolved_codegen = await CodegenConfigResolver(config_db).resolve_for_project(project_id)
    provider = (
        resolved_codegen.provider.slug if resolved_codegen else (plan_context or {}).get("codegen_provider_override")
        or (settings.ai_provider.value if hasattr(settings.ai_provider, "value") else str(settings.ai_provider))
    )
    if provider not in {
        AIProvider.CURSOR.value, AIProvider.OPENAI.value,
        AIProvider.CODEX.value, AIProvider.CLAUDE.value,
    }:
        raise RuntimeError(f"GitHub Actions codegen does not support provider {provider!r} yet")
    if not callback_token:
        raise RuntimeError("CODEGEN_ACTIONS_CALLBACK_TOKEN must be configured for GitHub Actions codegen")
    if not settings.codegen_actions_payload_secret:
        raise RuntimeError("CODEGEN_ACTIONS_PAYLOAD_SECRET must be configured for GitHub Actions codegen")

    async with db_factory() as db:
        project = await ProjectRepository(db).get_with_repo(project_id)
        if not project or not getattr(project, "repo", None):
            raise RuntimeError("Project repo not found")
        repo_full_name = project.repo.repo_full_name
        default_branch = getattr(project.repo, "default_branch", None) or "main"
        preview_branch = getattr(project.repo, "preview_branch", None) or settings.hosting_preview_branch or "preview"
        project_type = getattr(project, "type", "") or ""
        project_ref = (getattr(project, "project_ref", None) or "").strip()
        workspace_id = project.workspace_id
        project_created_at = getattr(project, "created_at", None)
        from src.domain.hosting.deployment_targets import is_w4p_project
        from src.domain.hosting.deploy_target import settings_for_project
        from src.domain.cloud_infra.repository import CFAccountRepository, CloudInstanceRepository, WorkspaceCloudAssignmentRepository
        from src.domain.cloud_infra.service import CloudInfraService

        async def _workspace_credentials(ws_id):
            infra = CloudInfraService(
                cf_account_repo=CFAccountRepository(db),
                instance_repo=CloudInstanceRepository(db),
                assignment_repo=WorkspaceCloudAssignmentRepository(db),
            )
            return await infra.get_credentials_for_workspace(ws_id)

        secret_settings = await settings_for_project(
            workspace_id, getattr(project, "created_at", None), settings, _workspace_credentials
        )
        from src.domain.cloud.facades import project_port_context
        port_context = await project_port_context(db, project)

    installation_id = await resolve_installation_id_for_workspace(db_factory, workspace_id)
    github = GitHubAppClient(installation_id=installation_id)

    if preview_branch != default_branch:
        preview_sha = await github.get_ref_head_sha(repo_full_name, preview_branch)
        if not preview_sha:
            default_sha = await github.get_ref_head_sha(repo_full_name, default_branch)
            if not default_sha:
                raise RuntimeError(f"Default branch {default_branch!r} has no HEAD commit")
            await github.create_branch_from_sha(repo_full_name, preview_branch, default_sha)

    # Run-scoped work branch (work_branch.py): created once per run from the preview head,
    # never force-pushed; the dispatch files and all agent commits live on it, and `preview`
    # is only advanced after validation. None keeps the legacy behaviour (commit on preview).
    work_state = await _ensure_work_branch(
        github, repo_full_name, run_id, preview_branch, plan_context, settings
    )
    commit_branch = work_state["branch"] if work_state else preview_branch
    if work_state:
        from src.domain.codegen.work_branch import persist_state, valid_state

        # Fail closed: a branch Core cannot remember cannot be resumed or trusted on retry, so
        # do not dispatch. Nothing has been written to preview at this point.
        if not await persist_state(db_factory, run_id, work_state):
            if not valid_state((plan_context or {}).get("work_branch"), run_id):
                try:  # created by this call: remove it so a retry starts clean
                    await github.delete_work_branch(repo_full_name, work_state["branch"])
                except Exception as exc:
                    logger.warning(
                        "work_branch_cleanup_after_persist_failure_failed",
                        run_id=str(run_id), error_type=type(exc).__name__,
                    )
            raise RuntimeError(
                "Could not save the work-branch state for this run; not dispatching "
                "(preview was not touched). Retry the run."
            )

    provider_secret = _provider_secret_name(provider)
    secrets = {
        "P2HS_CODEGEN_CALLBACK_TOKEN": callback_token,
        "P2HS_CODEGEN_PAYLOAD_SECRET": settings.codegen_actions_payload_secret,
    }
    provider_value = resolved_codegen.secret() if resolved_codegen else _provider_secret_value(provider)
    if provider_secret and provider_value:
        secrets[provider_secret] = provider_value
    cf_token = (secret_settings.cloudflare_api_token or "").strip()
    cf_account = (secret_settings.cloudflare_account_id or "").strip()
    if cf_token and cf_account:
        secrets["CLOUDFLARE_API_TOKEN"] = cf_token
        secrets["CLOUDFLARE_ACCOUNT_ID"] = cf_account
    await github.set_repo_action_secrets(repo_full_name, secrets)

    payload_path = f"{PAYLOAD_DIR}/{run_id}.json"
    prompt_media_manifest_path = f".p2hs-user-uploads/{run_id}/manifest.json"
    callback_url = f"{settings.api_url.rstrip('/')}/api/internal/codegen/actions/{run_id}/complete"
    progress_url = f"{settings.api_url.rstrip('/')}/api/internal/codegen/actions/{run_id}/progress"
    from types import SimpleNamespace

    from src.domain.codegen.record_adapter import platform_adapter_files

    continuation_meta = dict(continuation or {})
    continuation_attempt = int(continuation_meta.get("attempt") or 0)
    if continuation_attempt:
        from src.domain.codegen.prompt_wrapper import build_continuation_user_prompt

        user_prompt = build_continuation_user_prompt(
            user_prompt,
            attempt=continuation_attempt,
            max_attempts=int(settings.codegen_actions_max_continuations),
            last_summary=str(continuation_meta.get("summary") or ""),
        )
        media_bundle = SimpleNamespace(prompt=user_prompt, upload_dir="", files={}, cloud_refs={})
    else:
        media_bundle = await prepare_prompt_media_for_actions(
            db_factory=db_factory,
            project_id=project_id,
            run_id=run_id,
            base_user_prompt=user_prompt,
            plan_context=plan_context or {},
        )
    # Externally generated source preserved at start_build (import_snapshot.py): committed
    # unchanged next to this run so the agent copies it instead of re-typing the prompt
    # copy. Continuations reuse what the first dispatch already committed.
    import_files: dict[str, str | bytes] = {}
    dispatch_prompt = media_bundle.prompt
    if not continuation_attempt:
        from src.domain.codegen.import_snapshot import load_for_dispatch

        import_files, import_appendix = await load_for_dispatch(
            settings, workspace_id, project_id, plan_context
        )
        if import_appendix:
            dispatch_prompt = f"{dispatch_prompt}\n\n{import_appendix}"
    # A static prototype being migrated/replaced is converted to Nuxt pages here, deterministically
    # (static_conversion.py), so the agent adds wiring instead of retyping or statically serving the pages.
    conversion_files: dict[str, Any] = {}
    conversion_summary: Optional[dict[str, Any]] = None
    if not continuation_attempt:
        conversion_files, conversion_appendix, conversion_summary = await _static_conversion_for_dispatch(
            settings=settings, github=github, repo_full_name=repo_full_name, branch=commit_branch,
            workspace_id=workspace_id, project_id=project_id, project_type=project_type,
            plan_context=plan_context,
        )
        if conversion_appendix:
            dispatch_prompt = f"{dispatch_prompt}\n\n{conversion_appendix}"
    correction = (plan_context or {}).get("correction")
    if work_state and isinstance(correction, dict) and not continuation_attempt:
        from src.domain.codegen.work_branch import build_correction_appendix

        dispatch_prompt = (
            f"{dispatch_prompt}\n\n"
            f"{build_correction_appendix(correction, int(correction.get('attempt') or 1))}"
        )
    payload = {
        "run_id": str(run_id),
        "project_id": str(project_id),
        "provider": provider,
        "branch": preview_branch,
        # Work-branch mode (absent = legacy): the runner works on this branch, checkpoints to
        # it, and promotes to `branch` (preview) only after validation. `resume` says what a
        # retry may skip; work_base_sha is the preview head the branch was created from.
        "work_branch_name": work_state["branch"] if work_state else None,
        "work_base_sha": work_state["base_sha"] if work_state else None,
        "resume": (plan_context or {}).get("resume") if work_state else None,
        "callback_url": callback_url,
        "progress_url": progress_url,
        # The payload is HMAC-signed with CODEGEN_ACTIONS_PAYLOAD_SECRET before
        # it is committed. This grant is scoped to this run and lets the
        # P2HS-managed runner callback even if the legacy shared repo secret is
        # stale or unavailable.
        "callback_grant": create_codegen_action_grant_token(
            run_id, project_id, workspace_id
        ),
        "system_prompt": system_prompt,
        "user_prompt": dispatch_prompt,
        "project_type": project_type,
        "project_ref": project_ref,
        "context": {
            **dict(plan_context or {}),
            "prompt_media_upload_dir": media_bundle.upload_dir,
            "prompt_media_files": sorted(media_bundle.files.keys()),
            "prompt_media_cloud_refs": media_bundle.cloud_refs,
            "prompt_media_manifest_path": prompt_media_manifest_path if media_bundle.files else None,
        },
        "commit_message": f"p2hs: apply codegen run {str(run_id)[:8]}",
        "git_committer_name": settings.git_committer_name,
        "git_committer_email": "noreply@p2hs.com",
        "cursor_model": resolved_codegen.model.cli_model_value if resolved_codegen and provider == AIProvider.CURSOR.value else settings.cursor_model,
        "cursor_stream_progress": bool(settings.cursor_agent_stream_progress),
        "openai_model": resolved_codegen.model.cli_model_value if resolved_codegen and provider in {AIProvider.OPENAI.value, AIProvider.CODEX.value} else settings.openai_model,
        "claude_model": resolved_codegen.model.cli_model_value if resolved_codegen and provider == AIProvider.CLAUDE.value else settings.claude_model,
        "agent_budget_seconds": int(settings.codegen_actions_agent_budget_seconds),
        "deployment_required": False,
        # The default GITHUB_TOKEN actions/checkout persists does not trigger other
        # workflows on push (GitHub's anti-recursion rule), so the push this run makes
        # to `preview`/`main` would never start the separate deploy workflow. A token
        # scoped to only this repo restores normal push-triggers-workflow behavior
        # without handing the run installation-wide GitHub access.
        "git_push_token": (
            await github.mint_repo_scoped_token(repo_full_name)
            if project_ref and project_type in {"html", "react_app", "nuxt_app"}
            else None
        ),
        "record_adapter_files": platform_adapter_files(project_type, port_context),
        "continuation_attempt": continuation_attempt,
        "cursor_resume_session_id": continuation_meta.get("cursor_session_id") or None,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    envelope = {
        "payload": payload,
        "signature": _sign_payload(payload, settings.codegen_actions_payload_secret),
    }
    workflow_files = {
        WORKFLOW_PATH: build_codegen_actions_workflow(
            project_type,
            project_ref,
            w4p=is_w4p_project(project_type, project_created_at, settings),
        ),
        RUNNER_PATH: build_codegen_actions_runner(),
    }
    prompt_media_manifest = {
        "version": 1,
        "run_id": str(run_id),
        "project_id": str(project_id),
        "upload_dir": media_bundle.upload_dir,
        "files": media_bundle.cloud_refs,
        "notes": [
            "repo_path points to files committed for GitHub Actions codegen inspection.",
            "Platform file_id values are durable and can be used with platform import/session APIs.",
            "download_url values are signed convenience URLs and may expire.",
        ],
    }
    media_files: dict[str, str | bytes] = dict(media_bundle.files)
    if media_bundle.files:
        media_files[prompt_media_manifest_path] = _json_dumps(prompt_media_manifest)
    default_commit_sha: Optional[str] = None
    # GitHub requires a workflow_dispatch workflow to be present on the repository's
    # default branch. This can race with the final setup steps: discovery/codegen may
    # be submitted as soon as the preview is available, while the DB setup row is still
    # moving through its final stages. The setup push is explicitly rebase-safe, so the
    # workflow must be installed now rather than deferred based on that mutable DB flag.
    if default_branch != preview_branch:
        default_commit_sha = await github.create_commit(
            repo_full_name,
            default_branch,
            workflow_files,
            "[p2hs] ensure codegen workflow [skip ci]",
        )

    commit_sha = await github.create_commit(
        repo_full_name,
        commit_branch,
        {
            **workflow_files,
            **media_files,
            **import_files,
            **conversion_files,
            payload_path: _json_dumps(envelope),
        },
        f"[p2hs] dispatch codegen run {str(run_id)[:8]} [skip ci]",
    )
    dispatched = await github.trigger_workflow_dispatch(
        repo_full_name,
        settings.codegen_actions_workflow_file,
        commit_branch,
        {
            "run_id": str(run_id),
            "payload_path": payload_path,
            "cursor_model": payload["cursor_model"],
            "openai_model": payload["openai_model"],
            "claude_model": payload["claude_model"],
            "provider": provider,
            "continuation_attempt": str(continuation_attempt),
        },
    )
    if not dispatched:
        raise RuntimeError("GitHub workflow_dispatch was not accepted")
    return {
        "repo_full_name": repo_full_name,
        "branch": preview_branch,
        "work_branch": work_state["branch"] if work_state else None,
        "payload_path": payload_path,
        "prompt_media_upload_dir": media_bundle.upload_dir,
        "prompt_media_file_count": len(media_bundle.files),
        "prompt_media_manifest_path": prompt_media_manifest_path if media_bundle.files else None,
        "dispatch_commit_sha": commit_sha,
        "workflow_commit_sha": default_commit_sha,
        "static_conversion": conversion_summary,
        "continuation_attempt": continuation_attempt,
        "cursor_resume_session_id": continuation_meta.get("cursor_session_id") or None,
    }


async def dispatch_codegen_local_run(
    *,
    db_factory,
    run_id: UUID,
    project_id: UUID,
    device_id: UUID,
    system_prompt: str = "",
    user_prompt: str = "",
    plan_context: Optional[dict[str, Any]] = None,
    provider: str = "",
) -> dict[str, Any]:
    """Assign a codegen run to a paired desktop device for local execution.

    Sibling to ``dispatch_codegen_actions_run``: sets ``execution_target='local'``,
    ``device_id``, and ``status='queued'`` on the run, then publishes ``run.assigned`` on the
    device's per-device Redis channel (see ``src/core/pubsub.py``) so a currently-connected
    device (``/ws/devices/{device_id}``) can pick it up immediately. If the device is not
    connected, the run simply stays ``queued`` in Postgres — the desktop app polls
    ``GET /api/devices/{device_id}/runs?status=queued`` after heartbeat/reconnect, reusing the
    same durable-outbox spirit as ``ProjectJobOutbox`` without a new queue.

    The generation payload (system/user prompt, provider, branch, ...) is persisted into the
    run's existing ``checkpoint_payload`` JSONB column (worker_checkpoint="local_queued") rather
    than sent over the wire here — the device fetches it, along with a freshly minted
    ``run_grant``, via ``GET /api/devices/{device_id}/runs/{run_id}/envelope``. This mirrors
    ``dispatch_codegen_actions_run``'s payload shape but omits prompt-media/attachment fields:
    Phase 1 local execution does not support attachments. No HMAC signature is stored — that
    scheme exists only to protect the payload once it's committed into the user's repo for the
    GitHub Actions flow; the local envelope is served directly over an authenticated API call.

    Core remains authoritative: the device never touches the DB directly, only calls the
    generalized report endpoints (see ``require_run_grant_or_actions_token``).
    """
    from src.domain.codegen.repository import CodegenRunRepository
    from src.domain.projects.repository import ProjectRepository

    settings = get_settings()

    async with db_factory() as db:
        project = await ProjectRepository(db).get_with_repo(project_id)
        if not project or not getattr(project, "repo", None):
            raise RuntimeError("Project repo not found")
        preview_branch = (
            getattr(project.repo, "preview_branch", None)
            or settings.hosting_preview_branch
            or "preview"
        )

        repo = CodegenRunRepository(db)
        run = await repo.get_by_id(run_id)
        if not run:
            raise RuntimeError("Run not found")
        run.execution_target = "local"
        run.device_id = device_id
        run.status = "queued"
        run.completed_at = None

        local_payload = {
            "run_id": str(run_id),
            "project_id": str(project_id),
            "provider": provider,
            "branch": preview_branch,
            "system_prompt": system_prompt,
            "user_prompt": user_prompt,
            "context": dict(plan_context or {}),
            "commit_message": f"p2hs: apply codegen run {str(run_id)[:8]}",
            "git_committer_name": settings.git_committer_name,
            "git_committer_email": "noreply@p2hs.com",
            "cursor_model": settings.cursor_model,
            "cursor_stream_progress": bool(settings.cursor_agent_stream_progress),
            "openai_model": settings.openai_model,
            "claude_model": settings.claude_model,
            "result_delivery": "core_bundle_upload",
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
        await repo.update_checkpoint(run_id, "local_queued", local_payload)
        await db.commit()

    from src.core.pubsub import get_pubsub_redis, publish_device_event

    redis = await get_pubsub_redis()
    try:
        await publish_device_event(
            redis,
            str(device_id),
            "run.assigned",
            {"run_id": str(run_id), "project_id": str(project_id)},
        )
    finally:
        await redis.aclose()

    return {"executor": "local", "device_id": str(device_id)}
