import fsp from "node:fs/promises";
import path from "node:path";
import { currentBranch, currentCommit, runHasChanges, workspaceDiff, workspaceStatus } from "./git.js";
import { pathExists, readJson, runCommand, writeJson } from "./util.js";
import { agentContextPath, specPath, verifierPath } from "./state.js";
import { PROPAGATION_RULES } from "./surfaces.js";
import { deriveGoal, deriveSuccess, extractSuccess, formatGoalPrompt } from "./goal.js";
const INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md", "README.md"];
/**
 * Read the repo instruction files (AGENTS.md / CLAUDE.md / README.md) plus a
 * generated RUDDER.md if present, truncated for prompt budget. Shared by the
 * spec builder and the planner so both prime an agent with the same context.
 */
export async function loadInstructionFiles(workspace, repoRoot) {
    const instructionsFiles = [];
    for (const name of INSTRUCTION_FILES) {
        const filePath = path.join(workspace, name);
        if (!(await pathExists(filePath))) {
            continue;
        }
        const content = await fsp.readFile(filePath, "utf8");
        instructionsFiles.push({
            path: name,
            content: content.slice(0, 12_000),
        });
    }
    const contextFile = agentContextPath(repoRoot ?? workspace);
    if (await pathExists(contextFile)) {
        const content = await fsp.readFile(contextFile, "utf8");
        instructionsFiles.push({
            path: "RUDDER.md",
            content: content.slice(0, 12_000),
        });
    }
    return instructionsFiles;
}
export async function createSpec(run) {
    const workspace = run.worktree.path;
    const instructionsFiles = await loadInstructionFiles(workspace, run.repoRoot);
    const task = run.currentPrompt ?? run.task;
    const acceptanceCriteria = buildAcceptanceCriteria(task);
    const spec = {
        runId: run.id,
        task,
        // The plain `rudder <task>` / TUI single-task spawn has no planner-supplied
        // goal: the objective is the task statement, and the success condition is
        // the acceptance criteria. renderContract leads with these in objective format.
        // When the task is already an objective-formatted planner prompt, recover its
        // explicit objective + done-when instead of re-deriving.
        goal: deriveGoal(task),
        success: extractSuccess(task) ?? deriveSuccess(acceptanceCriteria),
        createdAt: new Date().toISOString(),
        repo: {
            root: workspace,
            branch: await currentBranch(workspace),
            baseCommit: (await currentCommit(workspace)) || run.baseCommit,
            status: await workspaceStatus(run),
        },
        instructionsFiles,
        acceptanceCriteria,
        suggestedTests: await suggestTests(workspace),
    };
    await writeJson(specPath(run.repoRoot, run.id), spec);
    return spec;
}
export function renderContract(spec) {
    const instructions = spec.instructionsFiles
        .map((file) => `### ${file.path}\n${file.content}`)
        .join("\n\n");
    const body = [
        "Rudder-specific context injected by Rudder",
        "This context was generated by Rudder. It is coordination context, not text typed by the user.",
        "",
        `Task: ${spec.task}`,
        "",
        "Acceptance criteria:",
        ...spec.acceptanceCriteria.map((item) => `- ${item}`),
        "",
        spec.suggestedTests.length > 0
            ? ["Suggested verification:", ...spec.suggestedTests.map((item) => `- ${item}`)].join("\n")
            : "Suggested verification: choose the smallest relevant checks for this repo.",
        "",
        "Working rules:",
        "- Read RUDDER.md first if it exists; it is generated by Rudder and lists other active Rudder agents and their worktrees. If RUDDER_SHARED.md exists beside it, read that too; it is gitignored user-shared context for tokens, env vars, private URLs, and local setup details that must survive model compaction.",
        "- Keep changes focused on the task.",
        "- Preserve unrelated user changes.",
        "- Run relevant checks when practical and report any checks that could not be run.",
        "- Do not mark the task complete unless the acceptance criteria are handled.",
        ...PROPAGATION_RULES.map((rule) => `- ${rule}`),
        "",
        instructions ? `Repository instructions:\n\n${instructions}` : "",
    ]
        .filter(Boolean)
        .join("\n");
    // Lead with the goal header so the backend sees the objective and verifiable
    // stopping condition before the body without invoking a slash command.
    return formatGoalPrompt({
        goal: spec.goal || deriveGoal(spec.task),
        success: spec.success || deriveSuccess(spec.acceptanceCriteria),
        body,
    });
}
export async function verifyRun(run) {
    const diff = await workspaceDiff(run);
    const changed = await runHasChanges(run);
    const outputPath = path.join(run.repoRoot, ".rudder", "runs", run.id, "output.txt");
    const transcript = await fsp.readFile(outputPath, "utf8").catch(() => "");
    const spec = await readJson(specPath(run.repoRoot, run.id));
    const missing = [];
    const satisfied = [];
    if (!changed) {
        if (expectsCodeChanges(run.task)) {
            missing.push("No file changes were detected.");
        }
        else {
            satisfied.push("No file changes were needed for this prompt.");
        }
    }
    else {
        satisfied.push(diff.trim() ? "The run produced a git diff." : "The run produced git status changes.");
    }
    if (/test|spec|check|lint|typecheck|verify/i.test(transcript)) {
        satisfied.push("The transcript mentions verification or tests.");
    }
    else if ((changed || expectsCodeChanges(run.task)) && spec?.suggestedTests.length) {
        missing.push("No explicit verification/test run was detected in the transcript.");
    }
    const result = {
        satisfied,
        missing,
        notes: changed
            ? "Local verifier checked for produced changes and evidence of verification."
            : expectsCodeChanges(run.task)
                ? "Local verifier found no changes. The worker may have only analyzed the task."
                : "Local verifier accepted a no-change response for a non-edit prompt.",
        shouldContinue: missing.some((item) => item.startsWith("No file changes")),
    };
    await writeJson(verifierPath(run.repoRoot, run.id), result);
    return result;
}
function expectsCodeChanges(task) {
    if (/\b(do not|don't|without|no)\s+(edit|change|modify|write|touch)\b/i.test(task)) {
        return false;
    }
    return /\b(add|build|change|create|delete|edit|fix|implement|modify|patch|refactor|remove|rename|repair|replace|rewrite|ship|update|write)\b/i.test(task);
}
function buildAcceptanceCriteria(task) {
    const criteria = ["Address the user's requested task directly.", "Keep the change scoped and reviewable."];
    if (/test|fail|bug|fix/i.test(task)) {
        criteria.push("Run or explain the relevant failing test/check.");
    }
    if (/refactor/i.test(task)) {
        criteria.push("Preserve existing behavior while improving structure.");
    }
    return criteria;
}
async function suggestTests(workspace) {
    const suggestions = [];
    const packageJson = await readJson(path.join(workspace, "package.json"));
    if (packageJson?.scripts) {
        for (const script of ["test", "check", "typecheck", "lint"]) {
            if (packageJson.scripts[script]) {
                suggestions.push(`npm run ${script}`);
            }
        }
    }
    if (await pathExists(path.join(workspace, "Cargo.toml"))) {
        suggestions.push("cargo test");
    }
    if (await pathExists(path.join(workspace, "pyproject.toml"))) {
        suggestions.push("pytest");
    }
    if (suggestions.length === 0) {
        const result = await runCommand("git", ["ls-files"], { cwd: workspace, allowFailure: true });
        if (result.stdout.includes("Makefile")) {
            suggestions.push("make test");
        }
    }
    return suggestions;
}
//# sourceMappingURL=brain.js.map