import { execFile } from "node:child_process"; import { promisify } from "node:util"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { budgetFor } from "../models/price-catalog.js"; import type { DispatchRequest, ProviderHealth } from "../types.js"; import { explicitAcceptanceCommands } from "../verification/infer-command.js"; import { analyzeTask } from "./task-shape.js"; const execFileAsync = promisify(execFile); /** * What the repository itself says about the task, read before any model sees it. These are the * signals routing may escalate on, so they are gathered from git and the request rather than * from anything an agent claims. */ export function checkoutPath(value: string): string { return value.trim().replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""); } export function withinScope(path: string, scopes: readonly string[]): boolean { const candidate = checkoutPath(path); return scopes.some((scope) => scope === "." || candidate === scope || candidate.startsWith(`${scope}/`)); } export function affectedDirectory(path: string): string { const parts = checkoutPath(path).split("/").filter(Boolean); if (["apps", "packages", "services"].includes(parts[0] ?? "") && parts[1]) return `${parts[0]}/${parts[1]}`; return parts.length > 1 ? parts[0]! : "."; } export function explicitCommandCount(request: DispatchRequest): number { const commands = new Set(); if (request.acceptanceCommand?.trim()) commands.add(request.acceptanceCommand.trim()); for (const command of explicitAcceptanceCommands(request.objective)) commands.add(command); return commands.size; } export async function checkoutPreflight(cwd: string, request: DispatchRequest, context: ExtensionContext, budget: ReturnType, health: ProviderHealth) { const git = async (args: string[]): Promise => { try { const result = await execFileAsync("git", ["-c", "core.fsmonitor=false", "-c", "diff.external=", ...args], { cwd, encoding: "utf8", timeout: 5_000, maxBuffer: 1_000_000, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } }); return String(result.stdout); } catch { return ""; } }; const [status, diff, staged] = await Promise.all([ git(["status", "--porcelain=v1", "--untracked-files=all"]), git(["diff", "--no-ext-diff", "--numstat", "--"]), git(["diff", "--cached", "--no-ext-diff", "--numstat", "--"]), ]); const scopePaths = analyzeTask(request).mentionedPaths.map(checkoutPath).filter(Boolean); const dirtyPaths = status.split("\n").filter(Boolean).map((line) => checkoutPath((line.slice(3).split(" -> ").at(-1) ?? ""))); const diffPaths = `${diff}\n${staged}`.split("\n").filter(Boolean).map((line) => checkoutPath(line.split("\t").at(-1) ?? "")); const scopedDirtyPaths = dirtyPaths.filter((path) => withinScope(path, scopePaths)); const scopedDiffPaths = diffPaths.filter((path) => withinScope(path, scopePaths)); const affectedPaths = [...new Set([...scopedDirtyPaths, ...scopedDiffPaths])]; const contextPercent = context.getContextUsage()?.percent; const sessionCacheState: "cold-proxy" | "warm-proxy" | "unknown" = typeof contextPercent !== "number" ? "unknown" : contextPercent < 15 ? "cold-proxy" : "warm-proxy"; const model = context.model ? `${context.model.provider}/${context.model.id}` : "unselected"; return { requestChars: request.objective.length, explicitPaths: scopePaths.length, explicitSymbols: [...request.objective.matchAll(/\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b/g)].length, explicitCommands: explicitCommandCount(request), acceptanceDeclared: Boolean(request.acceptanceCommand), dirtyFiles: new Set(dirtyPaths).size, diffStatFiles: new Set(diffPaths).size, scopedDirtyFiles: new Set(scopedDirtyPaths).size, scopedDiffStatFiles: new Set(scopedDiffPaths).size, affectedDirectories: new Set((affectedPaths.length ? affectedPaths : scopePaths).map(affectedDirectory)).size, currentRunFailureCount: 0, currentModel: model, contextRatio: typeof contextPercent === "number" ? contextPercent / 100 : undefined, sessionCacheState, remainingCredits: Math.max(0, budget.internalStopTarget - budget.spentCredits), providerState: health.state, }; }