import path from "node:path";
import { indexArtifactFile } from "../artifact-store/index.js";
import { appendEvent } from "../observability/index.js";
import { runDir } from "../run-state/index.js";
import { modelProfiles, seedAgentNames, seedTeamCapabilityProfiles, } from "../../config/schema.js";
import { LAB_REPRESENTATIVE_AGENT } from "../../agents/identity.js";
import { classifyToolName } from "../policy/index.js";
import { createId, ensureDir, normalizeId, nowIso, sanitizeText, writeJsonFile, writeTextFile } from "../../shared/index.js";
import { teamRoot } from "./store.js";
export const SEED_TEAM_SPEC_SCHEMA_VERSION = 1;
export const seedTeamCoverageAreas = ["planner", "executor", "verifier"];
const defaultRequiredCoverage = ["planner", "executor", "verifier"];
export function teamSpecsRoot(store, runId) {
    return path.join(teamRoot(store, runId), "specs");
}
export function teamSpecJsonPath(store, runId, teamId) {
    return path.join(teamSpecsRoot(store, runId), `${normalizeId(teamId)}.json`);
}
export function teamSpecMarkdownPath(store, runId, teamId) {
    return path.join(teamSpecsRoot(store, runId), `${normalizeId(teamId)}.md`);
}
export function validateTeamSpec(input, config) {
    const spec = normalizeTeamSpec(input, config);
    const issues = [];
    const raw = recordFrom(input);
    const maxMembers = Math.max(1, config.team_mode.max_members);
    const allowedCapabilities = new Set(config.team_mode.allowed_capability_profiles);
    const enabledBuiltinMcps = new Set(Object.entries(config.mcp.builtin)
        .filter(([name, value]) => value !== false && !config.disabled_mcps.includes(name))
        .map(([name]) => name));
    if (!spec.team_name)
        issue(issues, "required_team_name", "team_name", "team name is required");
    if (!spec.objective)
        issue(issues, "required_objective", "objective", "objective is required");
    if (!spec.lead_owner)
        issue(issues, "required_lead_owner", "lead_owner", "lead owner is required");
    if (!spec.authoritative_inputs.length)
        issue(issues, "required_authoritative_inputs", "authoritative_inputs", "at least one authoritative input is required");
    if (spec.expected_state_version < 1)
        issue(issues, "required_state_version", "expected_state_version", "expected state version must be a positive integer");
    if (!spec.expected_outputs.length)
        issue(issues, "required_expected_outputs", "expected_outputs", "at least one expected output is required");
    if (!spec.stop_condition)
        issue(issues, "required_stop_condition", "stop_condition", "stop condition is required");
    if (!spec.members.length)
        issue(issues, "required_members", "members", "at least one member is required");
    if (spec.members.length > maxMembers)
        issue(issues, "max_members", "members", `team has ${spec.members.length} members; maximum is ${maxMembers}`);
    for (const coverage of unknownStrings(raw.coverage)) {
        if (!isCoverageArea(coverage))
            issue(issues, "invalid_coverage", "coverage", `unknown coverage area: ${coverage}`);
    }
    const requiredCoverage = spec.workflow_template?.required_coverage.length
        ? spec.workflow_template.required_coverage
        : defaultRequiredCoverage;
    const covered = new Set(spec.members.flatMap((member) => member.coverage));
    for (const required of requiredCoverage) {
        if (!covered.has(required))
            issue(issues, "missing_coverage", "members.coverage", `required ${required} coverage is missing`);
    }
    const roleIds = new Set();
    const memberByRole = new Map();
    spec.members.forEach((member, index) => {
        const memberPath = `members.${index}`;
        if (!member.role_id)
            issue(issues, "required_role_id", `${memberPath}.role_id`, "role id is required");
        if (roleIds.has(member.role_id))
            issue(issues, "duplicate_role_id", `${memberPath}.role_id`, `duplicate role id: ${member.role_id}`);
        roleIds.add(member.role_id);
        memberByRole.set(member.role_id, member);
        if (!member.role_label)
            issue(issues, "required_role_label", `${memberPath}.role_label`, "role label is required");
        if (!member.mission_prompt)
            issue(issues, "required_mission_prompt", `${memberPath}.mission_prompt`, "mission prompt is required");
        if (member.mission_prompt.length > member.prompt_budget.max_prompt_chars) {
            issue(issues, "prompt_budget_exceeded", `${memberPath}.mission_prompt`, `mission prompt exceeds ${member.prompt_budget.max_prompt_chars} characters`);
        }
        if (member.prompt_budget.max_prompt_chars > config.team_mode.max_member_prompt_chars) {
            issue(issues, "prompt_budget_too_large", `${memberPath}.prompt_budget.max_prompt_chars`, `prompt budget exceeds configured maximum ${config.team_mode.max_member_prompt_chars}`);
        }
        if (!allowedCapabilities.has(member.capability_profile)) {
            issue(issues, "invalid_capability_profile", `${memberPath}.capability_profile`, `capability profile is not allowed: ${member.capability_profile}`);
        }
        if (member.resolved_agent && (!seedAgentNames.includes(member.resolved_agent) || config.disabled_agents.includes(member.resolved_agent))) {
            issue(issues, "invalid_resolved_agent", `${memberPath}.resolved_agent`, `resolved agent is not an enabled SEED agent: ${member.resolved_agent}`);
        }
        if (member.model_profile && !modelProfiles.includes(member.model_profile)) {
            issue(issues, "invalid_model_profile", `${memberPath}.model_profile`, `unknown model profile: ${member.model_profile}`);
        }
        if (member.model_route.model_profile && !modelProfiles.includes(member.model_route.model_profile)) {
            issue(issues, "invalid_model_route", `${memberPath}.model_route.model_profile`, `unknown model profile: ${member.model_route.model_profile}`);
        }
        for (const fallback of member.model_route.fallback_profiles) {
            if (!modelProfiles.includes(fallback))
                issue(issues, "invalid_model_route_fallback", `${memberPath}.model_route.fallback_profiles`, `unknown fallback model profile: ${fallback}`);
        }
        if (member.spawn_policy.max_depth < 0) {
            issue(issues, "invalid_spawn_policy", `${memberPath}.spawn_policy.max_depth`, "spawn max_depth must be zero or greater");
        }
        if (!member.spawn_policy.allowed && member.spawn_policy.max_depth > 0) {
            issue(issues, "invalid_spawn_policy", `${memberPath}.spawn_policy`, "spawn max_depth must be 0 when member spawning is disabled");
        }
        for (const toolName of member.tool_policy.allowed_tools) {
            if (config.disabled_tools.includes(toolName))
                issue(issues, "disabled_tool_allowed", `${memberPath}.tool_policy.allowed_tools`, `disabled tool is allowed by spec: ${toolName}`);
            if (!config.isolation.allow_host_tool_config && !isSeedOrNativeToolName(toolName)) {
                issue(issues, "host_tool_leakage", `${memberPath}.tool_policy.allowed_tools`, `host tool is not allowed in isolated team specs: ${toolName}`);
            }
        }
        for (const mcpName of member.mcp_policy.allowed_mcps) {
            if (config.disabled_mcps.includes(mcpName))
                issue(issues, "disabled_mcp_allowed", `${memberPath}.mcp_policy.allowed_mcps`, `disabled MCP is allowed by spec: ${mcpName}`);
            if (!config.isolation.allow_host_mcp && !enabledBuiltinMcps.has(mcpName)) {
                issue(issues, "host_mcp_leakage", `${memberPath}.mcp_policy.allowed_mcps`, `MCP is not a SEED builtin under isolated config: ${mcpName}`);
            }
        }
        if (member.skill_policy.allow_user_skills && !config.skills.allow_user_skills) {
            issue(issues, "user_skill_root_leakage", `${memberPath}.skill_policy.allow_user_skills`, "personal user skill roots are disabled by project config");
        }
        if (!config.skills.enabled && member.skill_policy.allowed_skills.length) {
            issue(issues, "skills_disabled", `${memberPath}.skill_policy.allowed_skills`, "skills are disabled by project config");
        }
        for (const skill of member.skill_policy.allowed_skills) {
            if (looksLikePersonalSkillPath(skill) && !config.skills.allow_user_skills) {
                issue(issues, "user_skill_root_leakage", `${memberPath}.skill_policy.allowed_skills`, `personal skill path is not allowed: ${skill}`);
            }
        }
        if (member.coverage.includes("verifier")) {
            if (!member.verifier_obligations.length)
                issue(issues, "required_verifier_obligations", `${memberPath}.verifier_obligations`, "verifier members must declare verifier obligations");
            if (!member.oracle_scope.length && !member.evidence_contract.oracle_scope.length) {
                issue(issues, "required_oracle_scope", `${memberPath}.oracle_scope`, "verifier members must declare oracle scope");
            }
        }
        if (member.human_in_the_loop.risk_tier === "high" && !member.human_in_the_loop.escalation_triggers.length) {
            issue(issues, "required_escalation_trigger", `${memberPath}.human_in_the_loop.escalation_triggers`, "high-risk members must declare escalation triggers");
        }
    });
    for (const member of spec.members) {
        for (const dependency of member.dependencies) {
            if (!memberByRole.has(dependency))
                issue(issues, "unknown_dependency", `members.${member.role_id}.dependencies`, `unknown dependency role id: ${dependency}`);
        }
    }
    for (let leftIndex = 0; leftIndex < spec.members.length; leftIndex += 1) {
        const left = spec.members[leftIndex];
        for (let rightIndex = leftIndex + 1; rightIndex < spec.members.length; rightIndex += 1) {
            const right = spec.members[rightIndex];
            for (const leftPath of left.write_paths) {
                for (const rightPath of right.write_paths) {
                    if (pathsOverlap(leftPath, rightPath) && !writeOverlapAllowed(left, right)) {
                        issue(issues, "write_lease_overlap", `members.${left.role_id}.write_paths`, `${left.role_id} and ${right.role_id} both request exclusive write access to ${leftPath} / ${rightPath}`);
                    }
                }
            }
        }
    }
    return { ok: !issues.some((item) => item.severity === "error"), spec, issues };
}
export async function acceptTeamSpec(store, runId, input, options = {}) {
    const validation = validateTeamSpec(input, store.config);
    const actor = options.actor ?? LAB_REPRESENTATIVE_AGENT;
    if (!validation.ok) {
        await appendEvent(store, runId, {
            type: "team_spec_rejected",
            actor,
            session_id: options.sessionId,
            summary: `Rejected dynamic team spec: ${validation.spec.team_name || validation.spec.team_id}`,
            metadata: { team_id: validation.spec.team_id, issues: validation.issues },
        });
        return { accepted: false, validation };
    }
    const timestamp = nowIso();
    const spec = { ...validation.spec, accepted_at: timestamp };
    const jsonPath = teamSpecJsonPath(store, runId, spec.team_id);
    const markdownPath = teamSpecMarkdownPath(store, runId, spec.team_id);
    await ensureDir(path.dirname(jsonPath));
    await writeJsonFile(jsonPath, spec);
    await writeTextFile(markdownPath, renderTeamSpecMarkdown(spec));
    await Promise.all([
        indexArtifactFile(store, runId, { file: jsonPath, name: `team_spec_${spec.team_id}`, extension: ".json" }),
        indexArtifactFile(store, runId, { file: markdownPath, name: `team_spec_${spec.team_id}`, extension: ".md" }),
    ]);
    await appendEvent(store, runId, {
        type: "team_spec_accepted",
        actor,
        session_id: options.sessionId,
        summary: `Accepted dynamic team spec: ${spec.team_name}`,
        metadata: {
            team_id: spec.team_id,
            member_count: spec.members.length,
            coverage: spec.coverage,
            json_path: path.relative(runDir(store, runId), jsonPath).split(path.sep).join("/"),
            markdown_path: path.relative(runDir(store, runId), markdownPath).split(path.sep).join("/"),
        },
    });
    return { accepted: true, spec, json_path: jsonPath, markdown_path: markdownPath, validation: { ...validation, spec } };
}
export function renderTeamSpecMarkdown(spec) {
    const lines = [
        `# ${sanitizeText(spec.team_name)}`,
        "",
        `Team ID: ${spec.team_id}`,
        `Objective: ${sanitizeText(spec.objective)}`,
        `Lead owner: ${spec.lead_owner}`,
        `Coverage: ${spec.coverage.join(", ")}`,
        spec.workflow_template ? `Workflow template: ${spec.workflow_template.name} (${spec.workflow_template.required_coverage.join(", ")})` : undefined,
        `Expected state version: ${spec.expected_state_version}`,
        `Accepted at: ${spec.accepted_at ?? "pending"}`,
        "",
        "## Members",
        "",
        ...spec.members.flatMap((member) => [
            `### ${member.role_id} - ${sanitizeText(member.role_label)}`,
            "",
            `- Coverage: ${member.coverage.join(", ")}`,
            `- Capability: ${member.capability_profile}`,
            `- Resolved agent: ${member.resolved_agent ?? "dynamic"}`,
            `- Model profile: ${member.model_profile ?? member.model_route.model_profile ?? "dynamic"}`,
            `- Spawn policy: allowed=${member.spawn_policy.allowed} max_depth=${member.spawn_policy.max_depth} approval=${member.spawn_policy.requires_lab_representative_approval}`,
            `- Write paths: ${member.write_paths.length ? member.write_paths.join(", ") : "none"}`,
            `- Stop condition: ${sanitizeText(member.stop_condition)}`,
            `- Expected outputs: ${member.expected_outputs.join(", ") || "none"}`,
            "",
            sanitizeText(member.mission_prompt),
            "",
        ]),
        "## Expected Outputs",
        "",
        ...spec.expected_outputs.map((output) => `- ${sanitizeText(output)}`),
        "",
        "## Stop Condition",
        "",
        sanitizeText(spec.stop_condition),
        "",
    ];
    return `${lines.filter((line) => line !== undefined).join("\n")}\n`;
}
function normalizeTeamSpec(input, config) {
    const raw = recordFrom(input);
    const timestamp = nowIso();
    const members = arrayOfRecords(raw.members).map((member) => normalizeTeamMember(member, config));
    const coverage = normalizeCoverage(raw.coverage);
    const derivedCoverage = uniqueCoverage([...coverage, ...members.flatMap((member) => member.coverage)]);
    const teamName = stringValue(raw.team_name) ?? stringValue(raw.name) ?? "";
    const teamId = normalizeId(stringValue(raw.team_id) ?? (teamName ? teamName : createId("team")));
    const workflowTemplate = normalizeWorkflowTemplate(raw.workflow_template);
    return {
        schema_version: SEED_TEAM_SPEC_SCHEMA_VERSION,
        team_id: teamId,
        team_name: teamName,
        objective: stringValue(raw.objective) ?? "",
        lead_owner: stringValue(raw.lead_owner) ?? LAB_REPRESENTATIVE_AGENT,
        ...(workflowTemplate ? { workflow_template: workflowTemplate } : {}),
        coverage: derivedCoverage,
        members,
        authoritative_inputs: stringArray(raw.authoritative_inputs),
        expected_state_version: intValue(raw.expected_state_version) ?? 0,
        read_paths: stringArray(raw.read_paths),
        write_paths: stringArray(raw.write_paths),
        evidence_paths: stringArray(raw.evidence_paths),
        artifact_paths: stringArray(raw.artifact_paths),
        expected_outputs: stringArray(raw.expected_outputs),
        stop_condition: stringValue(raw.stop_condition) ?? "",
        dependencies: stringArray(raw.dependencies),
        artifact_inputs: stringArray(raw.artifact_inputs),
        evidence_inputs: stringArray(raw.evidence_inputs),
        created_at: stringValue(raw.created_at) ?? timestamp,
        accepted_at: stringValue(raw.accepted_at),
    };
}
function normalizeTeamMember(raw, config) {
    const modelRouteRaw = recordValue(raw.model_route);
    const promptBudgetRaw = recordValue(raw.prompt_budget);
    const launchPolicyRaw = recordValue(raw.launch_policy);
    const spawnPolicyRaw = recordValue(raw.spawn_policy);
    const deliveryPolicyRaw = recordValue(raw.delivery_policy);
    const evidenceContractRaw = recordValue(raw.evidence_contract);
    const humanRaw = recordValue(raw.human_in_the_loop);
    const capability = stringValue(raw.capability_profile);
    const modelProfile = stringValue(raw.model_profile);
    const routeModelProfile = stringValue(modelRouteRaw.model_profile);
    return {
        role_id: normalizeId(stringValue(raw.role_id) ?? stringValue(raw.name) ?? createId("role")),
        role_label: stringValue(raw.role_label) ?? stringValue(raw.label) ?? "",
        coverage: normalizeCoverage(raw.coverage),
        capability_profile: (capability ?? "research"),
        resolved_agent: stringValue(raw.resolved_agent),
        resolved_category: stringValue(raw.resolved_category),
        model_profile: modelProfile,
        mission_prompt: stringValue(raw.mission_prompt) ?? stringValue(raw.prompt) ?? "",
        tool_policy: {
            allowed_tools: stringArray(recordValue(raw.tool_policy).allowed_tools),
            denied_tools: stringArray(recordValue(raw.tool_policy).denied_tools),
        },
        mcp_policy: {
            allowed_mcps: stringArray(recordValue(raw.mcp_policy).allowed_mcps),
            denied_mcps: stringArray(recordValue(raw.mcp_policy).denied_mcps),
        },
        skill_policy: {
            allowed_skills: stringArray(recordValue(raw.skill_policy).allowed_skills),
            denied_skills: stringArray(recordValue(raw.skill_policy).denied_skills),
            allow_user_skills: booleanValue(recordValue(raw.skill_policy).allow_user_skills) ?? false,
        },
        prompt_budget: {
            max_prompt_chars: intValue(promptBudgetRaw.max_prompt_chars) ?? config.team_mode.max_member_prompt_chars,
        },
        model_route: {
            model_profile: (routeModelProfile ?? defaultModelProfileForCapability(config, capability)),
            fallback_profiles: stringArray(modelRouteRaw.fallback_profiles),
        },
        spawn_policy: normalizeSpawnPolicy(spawnPolicyRaw, config),
        launch_policy: {
            mode: stringValue(launchPolicyRaw.mode) === "sync" ? "sync" : "background",
            wait_for_dependencies: booleanValue(launchPolicyRaw.wait_for_dependencies) ?? true,
            parallel_group: stringValue(launchPolicyRaw.parallel_group),
        },
        delivery_policy: {
            report_to: stringValue(deliveryPolicyRaw.report_to) ?? LAB_REPRESENTATIVE_AGENT,
            expected_artifact_paths: stringArray(deliveryPolicyRaw.expected_artifact_paths),
        },
        evidence_contract: {
            required_refs: stringArray(evidenceContractRaw.required_refs),
            required_artifacts: stringArray(evidenceContractRaw.required_artifacts),
            oracle_scope: stringArray(evidenceContractRaw.oracle_scope),
        },
        authoritative_inputs: stringArray(raw.authoritative_inputs),
        expected_state_version: intValue(raw.expected_state_version) ?? 0,
        read_paths: stringArray(raw.read_paths),
        write_paths: stringArray(raw.write_paths),
        evidence_paths: stringArray(raw.evidence_paths),
        artifact_paths: stringArray(raw.artifact_paths),
        expected_outputs: stringArray(raw.expected_outputs),
        stop_condition: stringValue(raw.stop_condition) ?? "",
        dependencies: stringArray(raw.dependencies).map(normalizeId),
        artifact_inputs: stringArray(raw.artifact_inputs),
        evidence_inputs: stringArray(raw.evidence_inputs),
        verifier_obligations: stringArray(raw.verifier_obligations),
        oracle_scope: stringArray(raw.oracle_scope),
        semantic_conflict_policy: semanticConflictPolicy(stringValue(raw.semantic_conflict_policy)),
        human_in_the_loop: {
            risk_tier: riskTier(stringValue(humanRaw.risk_tier)),
            escalation_triggers: stringArray(humanRaw.escalation_triggers),
        },
    };
}
function normalizeWorkflowTemplate(input) {
    const raw = recordValue(input);
    const name = stringValue(raw.name);
    if (!name)
        return undefined;
    return {
        name,
        required_coverage: normalizeCoverage(raw.required_coverage),
    };
}
function defaultModelProfileForCapability(config, capability) {
    return isCapabilityProfile(capability) ? config.virtual_member_defaults.capability_model_profiles[capability] : undefined;
}
function normalizeSpawnPolicy(input, config) {
    const defaults = config.virtual_member_defaults.default_spawn_policy;
    return {
        allowed: booleanValue(input.allowed) ?? defaults.allowed,
        max_depth: intValue(input.max_depth) ?? defaults.max_depth,
        requires_lab_representative_approval: booleanValue(input.requires_lab_representative_approval) ?? defaults.requires_lab_representative_approval,
    };
}
function issue(issues, code, pathValue, message) {
    issues.push({ code, path: pathValue, message, severity: "error" });
}
function isSeedOrNativeToolName(toolName) {
    const normalized = toolName.trim().toLowerCase();
    const nativeName = normalized.startsWith("opencode:") ? normalized.slice("opencode:".length) : normalized;
    const kind = classifyToolName(nativeName).kind;
    return kind === "read_only" ||
        kind === "write_file" ||
        kind === "shell" ||
        kind === "seed_workspace" ||
        kind === "seed_coordination" ||
        kind === "seed_delegation" ||
        kind === "seed_command_exec" ||
        kind === "seed_evidence_capture";
}
function writeOverlapAllowed(left, right) {
    if (left.semantic_conflict_policy === "human_approved_overlap" || right.semantic_conflict_policy === "human_approved_overlap")
        return true;
    const serialized = left.semantic_conflict_policy === "serialized" || right.semantic_conflict_policy === "serialized";
    const dependency = left.dependencies.includes(right.role_id) || right.dependencies.includes(left.role_id);
    return serialized && dependency;
}
function pathsOverlap(left, right) {
    const a = normalizePathForLease(left);
    const b = normalizePathForLease(right);
    if (!a || !b)
        return false;
    return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
}
function normalizePathForLease(value) {
    return value.replaceAll("\\", "/").replace(/\/+/g, "/").replace(/\/$/, "");
}
function looksLikePersonalSkillPath(value) {
    return value.startsWith("~") || path.isAbsolute(value);
}
function uniqueCoverage(values) {
    return seedTeamCoverageAreas.filter((area) => values.includes(area));
}
function normalizeCoverage(input) {
    return uniqueCoverage(unknownStrings(input).filter(isCoverageArea));
}
function isCoverageArea(value) {
    return seedTeamCoverageAreas.includes(value);
}
function isCapabilityProfile(value) {
    return value !== undefined && seedTeamCapabilityProfiles.includes(value);
}
function isModelProfile(value) {
    return value !== undefined && modelProfiles.includes(value);
}
function seedAgentName(value) {
    return value !== undefined && seedAgentNames.includes(value) ? value : undefined;
}
function semanticConflictPolicy(value) {
    if (value === "serialized" || value === "merge_review" || value === "human_approved_overlap")
        return value;
    return "exclusive";
}
function riskTier(value) {
    if (value === "medium" || value === "high")
        return value;
    return "low";
}
function recordFrom(input) {
    if (typeof input === "string") {
        try {
            return recordValue(JSON.parse(input));
        }
        catch {
            return {};
        }
    }
    return recordValue(input);
}
function recordValue(input) {
    return typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
}
function arrayOfRecords(input) {
    return Array.isArray(input) ? input.map(recordValue).filter((item) => Object.keys(item).length > 0) : [];
}
function stringArray(input) {
    return unknownStrings(input).map((item) => item.trim()).filter(Boolean);
}
function unknownStrings(input) {
    return Array.isArray(input) ? input.filter((item) => typeof item === "string") : [];
}
function stringValue(input) {
    return typeof input === "string" && input.trim().length > 0 ? input.trim() : undefined;
}
function intValue(input) {
    return typeof input === "number" && Number.isInteger(input) ? input : undefined;
}
function booleanValue(input) {
    return typeof input === "boolean" ? input : undefined;
}
//# sourceMappingURL=spec.js.map