/**
 * Interactive post-init setup for workflow-guide agents and workflows.
 *
 * Source of truth: `docs/reference/workflow-catalog.md`. The catalog below transcribes the
 * software-engineering area (workflows, stages, role slugs, and the guide's
 * ordered candidate lists). Guide candidates are dated research; each role's
 * first candidate whose harness is installed AND authenticated wins. No
 * candidate is admitted without an authenticated harness (fail closed).
 *
 * This module writes only current KXM project resources:
 *   - `.kxm/agents/<role-slug>.yaml`   (kxm.agent.v1)
 *   - `.kxm/workflows/<slug>.yaml`     (kxm.workflow.v1)
 *   - `.kxm/roles/<role-slug>.yaml`    (kxm.role.v2)
 *   - `.kxm/models/<route-id>.yaml`    (kxm.model.v2)
 *   - `plans/evidence/route-guide-qwen-pi.md` when a Pi binding matches that note
 *   - admitted selectors appended to `.kxm/routes.yaml`
 * It never writes retired legacy authority (`.kxm/config` or
 * `.kxm/roster.json`). Role and model files are the dispatch authority.
 *
 * Guide research ids are not dispatch ids. Only the admitted map below is
 * written, and only when that harness is authenticated. Unmapped ids are skipped.
 *
 * Google candidates are unmapped on purpose. Google's route is the Pi
 * `antigravity` provider, which only the KXM Pi extension registers. The
 * Runtime's Pi one-shot runs with `--no-extensions`, and `pi auth check` never
 * loads extensions, so a drive of an `antigravity/…` role fails at dispatch.
 * Mapping Google to `agy` instead would contradict the routing decision. Add
 * Google back only when drive can reach `antigravity` and a reviewed
 * admission decision says so.
 */

import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { stringify } from "yaml";
import { type HarnessInventory } from "./harness.ts";
import { loadRoutePolicy } from "./routes.ts";

export interface GuideCandidate {
  readonly vendor: string;
  readonly model: string;
}

export interface GuideStage {
  readonly slug: string;
  readonly title: string;
  readonly role: string;
  readonly domain: string;
  readonly candidates: readonly GuideCandidate[];
}

export interface GuideWorkflow {
  readonly slug: string;
  readonly area: string;
  readonly name: string;
  readonly summary: string;
  readonly stages: readonly GuideStage[];
}

/** Research id → the admitted harness/provider/model drive will actually accept.
 * Anything absent is skipped, even when its harness is logged in. */
const ADMITTED_GUIDE_BINDINGS: Readonly<Record<string, AgentBinding>> = Object.freeze({
  "anthropic/claude-fable-5.1": { harness: "claude", provider: "anthropic", model: "fable" },
  "anthropic/fable": { harness: "claude", provider: "anthropic", model: "fable" },
  "openai/gpt-5.6-sol": { harness: "codex", provider: "openai", model: "gpt-5.6-sol" },
  "x-ai/grok-4.6": { harness: "grok", provider: "xai", model: "grok-4.6" },
  "xai/grok-4.6": { harness: "grok", provider: "xai", model: "grok-4.6" },
  "qwen/qwen3.7-plus": { harness: "pi", provider: "openrouter", model: "qwen/qwen3.7-plus" },
});

/** Guide candidates are ordered by preference; the first eligible wins. */
export const GUIDE_WORKFLOWS: readonly GuideWorkflow[] = [
  {
    slug: "build-feature",
    area: "software-engineering",
    name: "Build Feature",
    summary: "PRD ingestion → system planning → spec/contract → scaffold → backend → frontend → SDK integration",
    stages: [
      {
        slug: "system-planning",
        title: "System planning",
        role: "lead-systems-planner",
        domain: "Decomposes requirements into an executable, dependency-ordered DAG of modules, tasks, and verification gates.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "openai", model: "gpt-6-astra-pro" },
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "qwen", model: "qwen3.8-max-0902" },
        ],
      },
      {
        slug: "spec-contract",
        title: "Spec and contract",
        role: "spec-contract-generator",
        domain: "Authors zero-ambiguity API contracts, type definitions, and schema validation rules before implementation begins.",
        candidates: [
          { vendor: "openai", model: "gpt-5.6-sol" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "google", model: "gemini-3.8-flash" },
          { vendor: "qwen", model: "qwen3.7-plus" },
          { vendor: "anthropic", model: "claude-fable-5.1" },
        ],
      },
      {
        slug: "scaffold",
        title: "Scaffold",
        role: "scaffold-build-specialist",
        domain: "Provisions monorepo workspace topologies, linters, Docker multi-stage builds, and CI pipelines.",
        candidates: [
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "mistralai", model: "devstral-2512" },
          { vendor: "qwen", model: "qwen3.7-plus" },
          { vendor: "openai", model: "gpt-5.3-codex" },
          { vendor: "kwaipilot", model: "kat-coder-pro-v2.5" },
        ],
      },
      {
        slug: "backend-data",
        title: "Backend and data",
        role: "backend-data-implementer",
        domain: "Implements domain entities, SQL queries/migrations, ORM persistence, concurrency primitives, and transactional boundaries.",
        candidates: [
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "qwen", model: "qwen3.7-plus" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "openai", model: "gpt-5.3-codex" },
          { vendor: "kwaipilot", model: "kat-coder-pro-v2.5" },
        ],
      },
      {
        slug: "frontend",
        title: "Frontend",
        role: "frontend-fullstack-implementer",
        domain: "Constructs stateful UI components, routing, client cache management, responsive styles, and accessible interactions.",
        candidates: [
          { vendor: "anthropic", model: "claude-sonnet-5" },
          { vendor: "google", model: "gemini-3.8-flash" },
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "bytedance-seed", model: "seed-2.0-code" },
          { vendor: "openai", model: "gpt-5.3-codex" },
        ],
      },
      {
        slug: "sdk-integration",
        title: "SDK integration",
        role: "sdk-integration-specialist",
        domain: "Wires external APIs, authentication flows (OAuth2/OIDC), webhook validation, cloud SDKs, and async worker queues.",
        candidates: [
          { vendor: "openai", model: "gpt-5.6-sol" },
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "qwen", model: "qwen3.7-plus" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "google", model: "gemini-3.8-flash" },
        ],
      },
    ],
  },
  {
    slug: "refactor-repair-regressions",
    area: "software-engineering",
    name: "Refactor and Repair Regressions",
    summary: "Codebase smell analysis → modular decomposition → surgical multi-file transforms → equivalence verification",
    stages: [
      {
        slug: "refactor-architecture",
        title: "Refactor architecture",
        role: "refactoring-architect",
        domain: "Analyzes code coupling, designs clean modular boundaries, and authors step-by-step non-breaking migration plans.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "z-ai", model: "glm-5.3" },
          { vendor: "anthropic", model: "claude-opus-5" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "moonshotai", model: "kimi-k3" },
        ],
      },
      {
        slug: "code-transform",
        title: "Code transform",
        role: "code-transform-implementer",
        domain: "Executes surgical multi-file edits, applying design patterns while preserving comments and formatting.",
        candidates: [
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "relace", model: "relace-apply-3" },
          { vendor: "openai", model: "gpt-5.3-codex" },
          { vendor: "morph", model: "morph-v3-large" },
          { vendor: "anthropic", model: "claude-sonnet-5" },
        ],
      },
      {
        slug: "equivalence-verification",
        title: "Equivalence verification",
        role: "semantic-equivalence-verifier",
        domain: "Critic-with-gate: reviews public contract compliance and behavioral equivalence; deterministic checks remain the fixed witness gate and are never delegated to an LLM.",
        candidates: [
          { vendor: "openai", model: "gpt-5.6-sol" },
          { vendor: "openai", model: "o3-mini-high" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "anthropic", model: "claude-opus-5" },
          { vendor: "mistralai", model: "devstral-2512" },
        ],
      },
    ],
  },
  {
    slug: "stabilize-flaky-tests",
    area: "software-engineering",
    name: "Stabilize Flaky Tests",
    summary: "Flakiness root-cause extraction → deterministic mock/async hardening",
    stages: [
      {
        slug: "flakiness-diagnosis",
        title: "Flakiness diagnosis",
        role: "concurrency-flakiness-detective",
        domain: "Untangles timing hazards, async wait conditions, shared state pollution, and unhandled promise rejections.",
        candidates: [
          { vendor: "openai", model: "o3" },
          { vendor: "deepseek", model: "deepseek-r1-0528" },
          { vendor: "google", model: "gemini-3.8-flash" },
          { vendor: "anthropic", model: "claude-opus-5" },
          { vendor: "openai", model: "gpt-6-astra" },
        ],
      },
    ],
  },
  {
    slug: "design-software-system",
    area: "software-engineering",
    name: "Design Software System",
    summary: "Architecture → database topology → threat modeling → IaC → reliability review → protocol audit",
    stages: [
      {
        slug: "distributed-architecture",
        title: "Distributed architecture",
        role: "principal-distributed-architect",
        domain: "Evaluates CAP/PACELC trade-offs, consensus protocols (Raft, Sagas), partition boundaries, and authors primary RFCs.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "openai", model: "o3" },
          { vendor: "openai", model: "gpt-6-astra-pro" },
          { vendor: "anthropic", model: "claude-opus-5" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
        ],
      },
      {
        slug: "database-topology",
        title: "Database topology",
        role: "scalability-database-topologist",
        domain: "Designs relational/NoSQL schemas, horizontal sharding keys, caching topologies, and zero-downtime migration plans.",
        candidates: [
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "openai", model: "gpt-6-astra-pro" },
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "qwen", model: "qwen3.8-2.4t-a95b" },
          { vendor: "x-ai", model: "grok-4.6" },
        ],
      },
      {
        slug: "threat-modeling",
        title: "Threat modeling",
        role: "security-architect-threat-modeler",
        domain: "Conducts STRIDE threat assessments, reviews IAM least-privilege policies, mTLS boundaries, and zero-trust architectures.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "openai", model: "gpt-5.6-sol" },
          { vendor: "openai", model: "gpt-6-astra-pro" },
          { vendor: "anthropic", model: "claude-opus-5" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
        ],
      },
      {
        slug: "iac-topology",
        title: "IaC topology",
        role: "iac-cloud-topology-engineer",
        domain: "Generates declarative Terraform/OpenTofu, Pulumi, Kubernetes CRDs, and VPC networking definitions.",
        candidates: [
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "qwen", model: "qwen3.7-plus" },
          { vendor: "openai", model: "gpt-5.3-codex" },
          { vendor: "google", model: "gemini-3.8-flash" },
          { vendor: "mistralai", model: "devstral-2512" },
        ],
      },
      {
        slug: "reliability-review",
        title: "Reliability review",
        role: "reliability-chaos-reviewer",
        domain: "Evaluates single points of failure, RPO/RTO metrics, cascading timeouts, circuit breakers, and active-active failovers.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "openai", model: "o3" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "openai", model: "gpt-6-astra-pro" },
          { vendor: "moonshotai", model: "kimi-k3" },
        ],
      },
      {
        slug: "protocol-audit",
        title: "Protocol audit",
        role: "architecture-critic-protocol-auditor",
        domain: "Independent peer-review; challenges unvalidated assumptions, prevents architectural drift, and verifies contracts.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "openai", model: "gpt-5.6-sol" },
          { vendor: "z-ai", model: "glm-5.3" },
          { vendor: "openai", model: "gpt-6-astra-pro" },
          { vendor: "qwen", model: "qwen3.8-max-0902" },
        ],
      },
    ],
  },
  {
    slug: "maintain-documentation",
    area: "software-engineering",
    name: "Maintain Documentation",
    summary: "Accuracy audit → documentation write → review",
    stages: [
      {
        slug: "accuracy-audit",
        title: "Accuracy audit",
        role: "documentation-accuracy-auditor",
        domain: "Read-only audit of developer and operator documentation against CLI --help outputs, source environment variables, tool schemas, and repository configuration.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "openai", model: "gpt-5.6-sol" },
          { vendor: "qwen", model: "qwen3.8-max-0902" },
          { vendor: "z-ai", model: "glm-5.3" },
        ],
      },
      {
        slug: "documentation-write",
        title: "Documentation write",
        role: "documentation-writer",
        domain: "Authors concise, accurate developer and operator documentation with clear terminology, correct command flags, and consistent formatting without marketing drift.",
        candidates: [
          { vendor: "x-ai", model: "grok-4.6" },
          { vendor: "qwen", model: "qwen3.7-plus" },
          { vendor: "anthropic", model: "claude-sonnet-5" },
          { vendor: "openai", model: "gpt-5.3-codex" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
        ],
      },
      {
        slug: "review",
        title: "Review",
        role: "documentation-reviewer",
        domain: "Critic-with-gate: the architecture critic verifies authority and permission wording; the CLI critic verifies command/flag accuracy; deterministic docs lint remains the fixed witness gate.",
        candidates: [
          { vendor: "anthropic", model: "claude-fable-5.1" },
          { vendor: "openai", model: "gpt-5.6-sol" },
          { vendor: "anthropic", model: "claude-opus-5" },
          { vendor: "deepseek", model: "deepseek-v4-pro-0813" },
          { vendor: "mistralai", model: "devstral-2512" },
        ],
      },
    ],
  },
];

export interface AgentBinding {
  readonly harness: string;
  readonly provider: string;
  readonly model: string;
}

export interface SkippedStage {
  readonly workflow: string;
  readonly stage: string;
  readonly role: string;
  readonly reason: string;
}

export interface GuideSetupPlan {
  readonly agents: ReadonlyMap<string, AgentBinding>;
  readonly workflows: GuideWorkflow[];
  readonly skipped: SkippedStage[];
}

export interface GuideSetupFile {
  readonly path: string;
  readonly content: string;
}

function authenticatedHarnesses(inventory: HarnessInventory): ReadonlySet<string> {
  return new Set(
    inventory.harnesses
      .filter((entry) => entry.detected && entry.authenticated === true)
      .map((entry) => entry.id),
  );
}

/**
 * Resolve a guide candidate to an admitted dispatch binding, or undefined when
 * no candidate is both on the admitted map and authenticated on its harness.
 */
export function resolveCandidate(
  candidates: readonly GuideCandidate[],
  eligible: ReadonlySet<string>,
): AgentBinding | undefined {
  for (const candidate of candidates) {
    const mapped = ADMITTED_GUIDE_BINDINGS[`${candidate.vendor}/${candidate.model}`];
    if (!mapped || !eligible.has(mapped.harness)) continue;
    return mapped;
  }
  return undefined;
}

/** Plan agents/workflows for the selected guide slugs without touching disk. */
export function planGuideSetup(options: {
  inventory: HarnessInventory;
  selected: readonly string[];
}): GuideSetupPlan {
  const eligible = authenticatedHarnesses(options.inventory);
  const agents = new Map<string, AgentBinding>();
  const workflows: GuideWorkflow[] = [];
  const skipped: SkippedStage[] = [];
  const wanted = new Set(options.selected);
  for (const workflow of GUIDE_WORKFLOWS) {
    if (!wanted.has(workflow.slug)) continue;
    let covered = true;
    for (const stage of workflow.stages) {
      if (agents.has(stage.role)) continue;
      const binding = resolveCandidate(stage.candidates, eligible);
      if (!binding) {
        skipped.push({
          workflow: workflow.slug,
          stage: stage.slug,
          role: stage.role,
          reason: "no admitted guide candidate has an authenticated harness (see `kxm harness list`)",
        });
        covered = false;
        continue;
      }
      agents.set(stage.role, binding);
    }
    if (covered) workflows.push(workflow);
    else skipped.push({ workflow: workflow.slug, stage: "*", role: "-", reason: "workflow skipped: at least one stage has no eligible agent" });
  }
  return { agents, workflows, skipped };
}

const WRITER_ROLE_PATTERN = /(writer|implementer|scaffold|backend|frontend|sdk-integration|iac-cloud|code-transform|build-specialist)/;

function isWriterRole(roleSlug: string): boolean {
  return WRITER_ROLE_PATTERN.test(roleSlug);
}

function agentDocument(roleSlug: string, stage: GuideStage): Record<string, unknown> {
  const writer = isWriterRole(roleSlug);
  return {
    schema: "kxm.agent.v1",
    purpose: `${stage.domain} (workflow-guide ${stage.title}; candidate verification is the operator's responsibility)`,
    role: roleSlug,
    tools: { preset: writer ? "workspace-writer" : "read-only" },
    defaultRepositoryAccess: writer ? "none" : "read",
    repositories: { control: writer ? "write" : "read" },
    network: "provider-only",
    resultSchema: "kxm.assignment-result.v1",
  };
}

function workflowDocument(workflow: GuideWorkflow): Record<string, unknown> {
  const steps = workflow.stages.map((stage, index) => {
    const last = index === workflow.stages.length - 1;
    return {
      id: stage.slug,
      kind: "agent",
      agent: stage.role,
      repositories: { control: isWriterRole(stage.role) ? "write" : "read" },
      maxAttempts: 2,
      on: {
        passed: last ? { target: "$terminal", terminalStatus: "completed" } : workflow.stages[index + 1]!.slug,
        failed: { target: "$terminal", terminalStatus: "failed" },
      },
    };
  });
  return {
    schema: "kxm.workflow.v1",
    description: `${workflow.name} (${workflow.area}, workflow-guide): ${workflow.summary}`,
    coordinator: "coordinator",
    limits: { maxTransitions: Math.max(4, workflow.stages.length * 4) },
    steps,
  };
}

/**
 * Evidence written into the user's project for the one admitted guided Pi model.
 * The hash is of these exact bytes. A Pi binding that is not this vendor, model
 * id, and harness gets no model file and no origin.
 */
const GUIDE_PI_EVIDENCE_SOURCE = "plans/evidence/route-guide-qwen-pi.md";
const GUIDE_PI_EVIDENCE_SHA256 = "8a24a7dc6db416df2153cfec7bccf1a4462f838e35c4e6707dc421ca84cf069b";
const GUIDE_PI_NOTE = {
  harness: "pi",
  vendor: "openrouter",
  model: "qwen/qwen3.7-plus",
  reason: "Qwen has no supported native harness on this runner, and the operator admitted that exact model for guided setup.",
  provingCommand: "pi auth check --provider openrouter",
} as const;

const GUIDE_PI_EVIDENCE_NOTE = `---
schema: "kxm.doc.v1"
id: "RES-GUIDE-QWEN-PI"
type: "research"
title: "Research: guided setup Pi Qwen origin"
project: "kxm"
status: "approved"
owner: "@operator"
created: "2026-09-26"
updated: "2026-09-26"
authority: "evidence"
confidence: "verified"
summary: "Guided setup pins every Pi model origin to this note so a later project.yaml edit cannot change the hash."
tags: ["routing", "qwen", "pi", "init-guide"]
related: []
details:
  research_status: "complete"
  target_decision_date: "2026-09-26"
---

# route-guide-qwen-pi

Dated 2026-09-26. Route ids are the guided role slugs. Harness \`${GUIDE_PI_NOTE.harness}\`. Model id \`${GUIDE_PI_NOTE.model}\`. Vendor \`${GUIDE_PI_NOTE.vendor}\`. Admitted because ${GUIDE_PI_NOTE.reason} Auth was proved with \`${GUIDE_PI_NOTE.provingCommand}\`.
`;

function guidePiNoteMatches(binding: AgentBinding): boolean {
  return binding.harness === GUIDE_PI_NOTE.harness
    && binding.provider === GUIDE_PI_NOTE.vendor
    && binding.model === GUIDE_PI_NOTE.model;
}

/**
 * Render the planned KXM resource files (`.kxm/agents/*.yaml`,
 * `.kxm/workflows/*.yaml`, and the Pi evidence note when a binding matches it).
 * The Pi origin is the hash of `plans/evidence/route-guide-qwen-pi.md` written
 * into the project, whether or not `.kxm/project.yaml` exists.
 */
export function renderGuideSetupFiles(projectRoot: string, plan: GuideSetupPlan): GuideSetupFile[] {
  const files: GuideSetupFile[] = [];
  const roleStages = new Map<string, GuideStage>();
  for (const workflow of plan.workflows) {
    for (const stage of workflow.stages) roleStages.set(stage.role, stage);
  }
  let evidenceQueued = false;
  for (const [role, binding] of [...plan.agents.entries()].sort(([a], [b]) => a.localeCompare(b))) {
    const stage = roleStages.get(role);
    if (!stage) continue;
    const routeId = role.replaceAll("_", "-");
    const writer = isWriterRole(role);
    const piMatch = binding.harness === "pi" && guidePiNoteMatches(binding);
    if (binding.harness === "pi" && !piMatch) {
      files.push({
        path: join(projectRoot, ".kxm", "agents", `${role}.yaml`),
        content: stringify(agentDocument(role, stage)),
      });
      continue;
    }
    const modelDocument: Record<string, unknown> = {
      schema: "kxm.model.v2",
      id: routeId,
      harness: binding.harness,
      model: binding.model,
      vendor: binding.provider,
      status: "admitted",
      permissions: [writer ? "edit" : "read-only"],
    };
    if (piMatch) {
      modelDocument.origin = { source: GUIDE_PI_EVIDENCE_SOURCE, sha256: GUIDE_PI_EVIDENCE_SHA256 };
      if (!evidenceQueued) {
        files.push({
          path: join(projectRoot, GUIDE_PI_EVIDENCE_SOURCE),
          content: GUIDE_PI_EVIDENCE_NOTE,
        });
        evidenceQueued = true;
      }
    }
    files.push({
      path: join(projectRoot, ".kxm", "agents", `${role}.yaml`),
      content: stringify(agentDocument(role, stage)),
    });
    files.push({
      path: join(projectRoot, ".kxm", "models", `${routeId}.yaml`),
      content: stringify(modelDocument),
    });
    files.push({
      path: join(projectRoot, ".kxm", "roles", `${role}.yaml`),
      content: stringify({
        schema: "kxm.role.v2",
        id: role,
        purpose: writer ? "writer" : "experiment",
        permission: writer ? "edit" : "read-only",
        description: stage.domain.slice(0, 2000),
        roster: [{ route: routeId }],
      }),
    });
  }
  for (const workflow of plan.workflows) {
    files.push({
      path: join(projectRoot, ".kxm", "workflows", `${workflow.slug}.yaml`),
      content: stringify(workflowDocument(workflow)),
    });
  }
  return files;
}

/** Append the plan's admitted selectors to `.kxm/routes.yaml`. Does not write role files. */
export function mergeGuideRouteAdmission(projectRoot: string, plan: GuideSetupPlan): string[] {
  const policy = loadRoutePolicy(projectRoot);
  const added: string[] = [];
  for (const binding of plan.agents.values()) {
    const selector = `${binding.provider}/${binding.model}`;
    if (policy.disabled.includes(selector) || policy.admitted.includes(selector)) continue;
    policy.admitted.push(selector);
    added.push(selector);
  }
  if (added.length === 0) return added;
  policy.updatedAt = new Date().toISOString();
  const path = join(projectRoot, ".kxm", "routes.yaml");
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, stringify(policy), "utf8");
  return added;
}

export interface WriteReport {
  readonly written: string[];
  readonly existed: string[];
}

/** Write rendered files, creating directories as needed; never overwrites. */
export function writeGuideSetupFiles(files: readonly GuideSetupFile[]): WriteReport {
  const written: string[] = [];
  const existed: string[] = [];
  for (const file of files) {
    if (existsSync(file.path)) {
      existed.push(file.path);
      continue;
    }
    mkdirSync(dirname(file.path), { recursive: true });
    writeFileSync(file.path, file.content, "utf8");
    written.push(file.path);
  }
  return { written, existed };
}

/** Parse a prompt answer like "1,3", "build-feature, all", or "none". */
export function parseGuideSelection(input: string, catalog: readonly GuideWorkflow[] = GUIDE_WORKFLOWS): string[] {
  const trimmed = input.trim().toLowerCase();
  if (!trimmed || trimmed === "none" || trimmed === "n" || trimmed === "no") return [];
  if (trimmed === "all") return catalog.map((workflow) => workflow.slug);
  const selected = new Set<string>();
  for (const token of trimmed.split(/[,\s]+/).filter(Boolean)) {
    const index = Number.parseInt(token, 10);
    if (Number.isFinite(index) && index >= 1 && index <= catalog.length) {
      selected.add(catalog[index - 1]!.slug);
      continue;
    }
    const match = catalog.find((workflow) => workflow.slug === token);
    if (match) selected.add(match.slug);
  }
  return [...selected];
}
