import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { createHash, randomBytes } from "node:crypto";
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
import { spawn, spawnSync } from "node:child_process";

// ── Types ──────────────────────────────────────────────────────────────
type JsonObject = Record<string, unknown>;
type TextContent = { type: "text"; text: string };
interface PiToolResult {
  content: TextContent[];
  details?: unknown;
}
interface PiExtensionContext {
  hasUI?: boolean;
  sessionManager?: {
    getSessionId?: () => string;
    getSessionFile?: () => string | undefined;
  };
  ui?: {
    notify?: (msg: string, type?: "info" | "warning" | "error") => void;
  };
}
interface SubagentInput {
  agent?: string;
  prompt?: string;
  mode?: "single" | "parallel" | "chain";
  prompts?: string[];
  model?: string;
  thinking?: string;
}
interface AgentConfig {
  model?: string;
  thinking?: string;
  tools?: string[];
  fallbackModels: string[];
}
interface PiRunConfig {
  model?: string;
  thinking?: string;
  tools?: string[];
}

// ── Lazy-load pi-tui (avoid failing top-level imports) ─────────────────
let _piTui: {
  visibleWidth?: (s: string) => number;
  truncateToWidth?: (s: string, w: number, ellipsis?: string) => string;
} | null = null;
function piTui() {
  if (!_piTui) {
    try {
      _piTui = require("@earendil-works/pi-tui");
    } catch {
      _piTui = {};
    }
  }
  return _piTui;
}
function trunc(s: string, w: number) {
  const t = piTui();
  return t.truncateToWidth
    ? t.truncateToWidth(s, w, "…")
    : s.length <= w
      ? s
      : w > 1
        ? s.slice(0, w - 1) + "…"
        : s.slice(0, w);
}

// ── Constants ─────────────────────────────────────────────────────────
const TRELLIS_AGENT_JSONL: Record<string, string> = {
  "trellis-implement": "implement.jsonl",
  implement: "implement.jsonl",
  "trellis-check": "check.jsonl",
  check: "check.jsonl",
};
const MAX_STDOUT = 8 * 1024 * 1024;
const MAX_STDERR = 1024 * 1024;
const MAX_TAIL = 256 * 1024;
const MAX_LINE_BUFFER = 1024 * 1024;
const MAX_TOOL_ARG_CHARS = 2048;
const MAX_TOOLS = 256;
const MAX_PARALLEL_PROMPTS = 6;
const ABORT_KILL_GRACE_MS = 1500;
const SESSION_OVERVIEW_TIMEOUT_MS = 1500;
const THROTTLE_MS = 500;
const FIRST_REPLY_NOTICE = `<first-reply-notice>
First visible reply: say once in Chinese that Trellis SessionStart context is loaded, then answer directly.
This notice is one-shot: do not repeat it after the first assistant reply in the same session.
</first-reply-notice>`;

// ── State types ───────────────────────────────────────────────────────
type RunStatus = "pending" | "running" | "succeeded" | "failed" | "cancelled";
type ToolStatus = "running" | "succeeded" | "failed";

interface Usage {
  input: number;
  output: number;
  cacheRead: number;
  cacheWrite: number;
  cost: number;
  ctxTokens: number;
  turns: number;
}
interface ToolTrace {
  id: string;
  name: string;
  args: string;
  status: ToolStatus;
  startedAt: number;
  finishedAt?: number;
}
interface RunState {
  id: string;
  agent: string;
  prompt: string;
  step?: number;
  status: RunStatus;
  startedAt?: number;
  finishedAt?: number;
  finalText: string;
  textTail: string;
  thinkingTail: string;
  stderrTail: string;
  tools: ToolTrace[];
  usage: Usage;
  model?: string;
  thinking?: string;
  errorMessage?: string;
}
interface ProgressDetails {
  kind: "trellis-subagent-progress";
  agent: string;
  mode: "single" | "parallel" | "chain";
  startedAt: number;
  updatedAt: number;
  final: boolean;
  runs: RunState[];
}

// ── Native partial-update card state ──────────────────────────────────
interface NativeCardHandle {
  state: JsonObject;
  invalidate: () => void;
  updatedAt: number;
}
const MAX_NATIVE_CARDS = 20;
const nativeCards = new Map<string, NativeCardHandle>();
let activeSubagentToolCallId: string | null = null;
function rememberNativeCard(id: string, card: NativeCardHandle) {
  nativeCards.set(id, card);
  const active = activeSubagentToolCallId
    ? nativeCards.get(activeSubagentToolCallId)
    : undefined;
  if (!active || card.updatedAt >= active.updatedAt)
    activeSubagentToolCallId = id;
  for (const key of nativeCards.keys()) {
    if (nativeCards.size <= MAX_NATIVE_CARDS) break;
    if (key !== activeSubagentToolCallId) nativeCards.delete(key);
  }
}
function totalUsage(d: ProgressDetails): Usage {
  const u: Usage = {
    input: 0,
    output: 0,
    cacheRead: 0,
    cacheWrite: 0,
    cost: 0,
    ctxTokens: 0,
    turns: 0,
  };
  for (const r of d.runs) {
    u.input += r.usage.input;
    u.output += r.usage.output;
    u.cacheRead += r.usage.cacheRead;
    u.cacheWrite += r.usage.cacheWrite;
    u.cost += r.usage.cost;
    u.ctxTokens = Math.max(u.ctxTokens, r.usage.ctxTokens);
    u.turns += r.usage.turns;
  }
  return u;
}
function activeRun(d: ProgressDetails) {
  return d.runs.find((r) => r.status === "running") ?? d.runs.at(-1);
}
function toolArgs(t: ToolTrace) {
  try {
    return JSON.parse(t.args) as Record<string, unknown>;
  } catch {
    return {};
  }
}
function bashCommand(t: ToolTrace) {
  const a = toolArgs(t);
  return String(a.command || "").toLowerCase();
}
function isSearchTool(t: ToolTrace) {
  return t.name === "read" || t.name === "grep" || t.name === "find";
}
function isMutationTool(t: ToolTrace) {
  return t.name === "edit" || t.name === "write";
}
function isValidationCommand(t: ToolTrace) {
  const c = bashCommand(t);
  return /\b(test|typecheck|lint|build|gofmt|go test|npm run|pnpm|vitest|tsc)\b/.test(
    c,
  );
}
function isInspectionCommand(t: ToolTrace) {
  const c = bashCommand(t);
  return /\b(rg|grep|find|git diff|git status|ls|tree)\b/.test(c);
}
function thinkingIntent(text: string) {
  const s = text.toLowerCase();
  if (/error|failed|failure|panic|exception|报错|失败|错误|异常/.test(s))
    return "Analyzing failure cause";
  if (/test|verify|check|typecheck|lint|验证|测试|检查/.test(s))
    return "Planning verification steps";
  if (/plan|approach|design|strategy|方案|计划|思路|设计/.test(s))
    return "Structuring the implementation approach";
  if (/implement|change|edit|modify|refactor|实现|修改|重构/.test(s))
    return "Reasoning through code changes";
  if (/inspect|search|locate|read|context|定位|搜索|阅读|上下文/.test(s))
    return "Locating relevant context";
  return "";
}
function behaviorSummary(r: RunState) {
  if (r.status === "succeeded") return "Task completed and result returned";
  if (r.status === "failed")
    return "Task failed and error details were retained";

  const runningTool = r.tools.findLast((t) => t.status === "running");
  if (runningTool) {
    if (isMutationTool(runningTool)) return "Applying the plan to code";
    if (runningTool.name === "bash" && isValidationCommand(runningTool))
      return "Verifying whether the implementation passes";
    if (runningTool.name === "bash" && isInspectionCommand(runningTool))
      return "Inspecting current code state";
    if (isSearchTool(runningTool)) return "Locating relevant code and context";
    if (runningTool.name === "bash")
      return "Validating assumptions with commands";
    return "Using tools to advance the task";
  }

  const recent = r.tools.slice(-5);
  if (recent.some((t) => t.status === "failed"))
    return "Investigating tool or command failure";
  if (recent.some(isMutationTool)) return "Reviewing recent changes";
  if (recent.some((t) => t.name === "bash" && isValidationCommand(t)))
    return "Analyzing verification results";
  if (
    recent.length >= 2 &&
    recent.every(
      (t) => isSearchTool(t) || (t.name === "bash" && isInspectionCommand(t)),
    )
  )
    return "Mapping code structure and impact";

  const intent = thinkingIntent(`${r.thinkingTail}\n${r.textTail}`);
  if (intent) return intent;
  if (!r.tools.length) return "Understanding the task and planning execution";
  return "Advancing the task and preparing next steps";
}
function progressState(d: ProgressDetails) {
  const running = d.runs.filter((r) => r.status === "running").length;
  const failed = d.runs.some((r) => r.status === "failed");
  return failed
    ? "failed"
    : d.final
      ? "completed"
      : running
        ? `${running} running`
        : "pending";
}
function progressDone(d: ProgressDetails) {
  return d.runs.filter((r) => r.status !== "pending" && r.status !== "running")
    .length;
}
function summaryText(text: string) {
  return `${text.trim().replace(/[。.!?…]+$/u, "")}...`;
}
function splitModelThinking(model?: string, fallbackThinking?: string) {
  const m = model?.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
  return {
    model: m ? m[1] : model,
    thinking: (m?.[2] ?? fallbackThinking)?.toLowerCase(),
  };
}
function modelLabel(r: RunState) {
  const { model, thinking } = splitModelThinking(r.model, r.thinking);
  if (!model) return undefined;
  return thinking && thinking !== "off" ? `${model}(${thinking})` : model;
}
function applyRunConfig(r: RunState, cfg: PiRunConfig) {
  const parsed = splitModelThinking(cfg.model, cfg.thinking);
  r.model = parsed.model;
  r.thinking = parsed.thinking;
}
function runElapsed(d: ProgressDetails, r: RunState) {
  const start = r.startedAt ?? d.startedAt;
  const end =
    r.finishedAt ?? (r.status === "running" ? Date.now() : d.updatedAt);
  return fmtDur(Math.max(0, end - start));
}
function runHeader(d: ProgressDetails, r: RunState) {
  const usage = fmtUsage(r.usage, modelLabel(r)) || fmtUsage(totalUsage(d));
  return `${r.agent} · ${progressDone(d)}/${d.runs.length} done · ${progressState(d)} · ${runElapsed(d, r)}${usage ? ` · ${usage}` : ""}`;
}
function renderRunBlock(
  lines: string[],
  d: ProgressDetails,
  run: RunState,
  expanded: boolean,
) {
  const step = run.step ? `step ${run.step} · ` : "";
  lines.push(`  - ${step}${runHeader(d, run)}`);
  const summary = behaviorSummary(run);
  if (summary) lines.push(`    › ${summaryText(summary)}`);
  const visibleTools = expanded ? run.tools.slice(-8) : run.tools.slice(-1);
  for (const t of visibleTools)
    lines.push(`    ${toolIcon(t.status)} ${toolBrief(t)}`);
  if (expanded && run.errorMessage) {
    lines.push(`    ✗ ${oneLine(run.errorMessage, 120)}`);
  }
}
function renderProgressCard(
  d: ProgressDetails,
  expanded: boolean,
  w: number,
): string[] {
  const r = activeRun(d);
  if (!r) return [];
  const spinner = ["◐", "◓", "◑", "◒"][Math.floor(Date.now() / 250) % 4]!;
  const icon = d.final
    ? d.runs.some((x) => x.status === "failed")
      ? "✗"
      : "✓"
    : spinner;
  const totalElapsed = fmtDur(
    (d.final ? d.updatedAt : Date.now()) - d.startedAt,
  );
  const lines: string[] = [
    `${icon} subagent ${d.mode} · total ${totalElapsed}`,
  ];

  if (!expanded) {
    renderRunBlock(lines, d, r, false);
    lines.push("  Alt+O expand latest subagent card");
    return lines.map((l) => trunc(l, w));
  }

  for (const run of d.runs) renderRunBlock(lines, d, run, true);
  lines.push("  Alt+O collapse latest subagent card");
  const max = 48;
  const shown =
    lines.length > max
      ? [
          ...lines.slice(0, max - 1),
          `  … ${lines.length - max + 1} lines hidden`,
        ]
      : lines;
  return shown.map((l) => trunc(l, w));
}
function progressKey(d: ProgressDetails) {
  return d.runs
    .map((r) => {
      const t = r.tools.at(-1);
      return [
        r.id,
        r.status,
        r.tools.length,
        t?.id ?? "",
        t?.status ?? "",
        r.usage.turns,
        r.usage.input,
        r.usage.output,
        r.usage.cacheRead,
        r.usage.cacheWrite,
        r.usage.ctxTokens,
        r.model ?? "",
        r.thinking ?? "",
        r.errorMessage ?? "",
      ].join("~");
    })
    .join("|");
}

// ── Utilities ─────────────────────────────────────────────────────────
function isObj(v: unknown): v is JsonObject {
  return typeof v === "object" && v !== null && !Array.isArray(v);
}
function str(v: unknown): string | null {
  return typeof v === "string" && v.trim() ? v.trim() : null;
}
function num(v: unknown): number {
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
}
function hash(s: string) {
  return createHash("sha256").update(s).digest("hex").slice(0, 24);
}
function readText(p: string) {
  try {
    return readFileSync(p, "utf-8");
  } catch {
    return "";
  }
}
function exists(p: string) {
  try {
    return statSync(p).isFile();
  } catch {
    return false;
  }
}
function shellQuote(v: string) {
  return `'${v.replace(/'/g, `'\\''`)}'`;
}
function callStr(cb: (() => string | undefined) | undefined): string | null {
  if (!cb) return null;
  try {
    return str(cb());
  } catch {
    return null;
  }
}
function lookupStr(data: unknown, keys: string[]): string | null {
  if (!isObj(data)) return null;
  for (const k of keys) {
    const v = str(data[k]);
    if (v) return v;
  }
  for (const nk of [
    "input",
    "properties",
    "event",
    "hook_input",
    "hookInput",
  ]) {
    const nested = data[nk];
    const v = lookupStr(nested, keys);
    if (v) return v;
  }
  return null;
}
function cmdHasTrellisCtx(cmd: string) {
  const t = cmd.trimStart();
  return (
    /^export\s+TRELLIS_CONTEXT_ID=/.test(t) ||
    /^TRELLIS_CONTEXT_ID=/.test(t) ||
    /^env\s+.*TRELLIS_CONTEXT_ID=/.test(t)
  );
}
function fmtDur(ms: number) {
  if (ms < 1000) return `${ms}ms`;
  const s = Math.floor(ms / 1000);
  if (s < 60) return `${s}s`;
  return `${Math.floor(s / 60)}m${s % 60}s`;
}
function fmtNum(n: number) {
  if (!n) return "0";
  if (Math.abs(n) < 1000) return `${n}`;
  if (Math.abs(n) < 1000000) return `${(n / 1000).toFixed(1)}k`;
  return `${(n / 1000000).toFixed(1)}m`;
}
function fmtUsage(u: Usage, m?: string) {
  const p: string[] = [];
  if (u.turns) p.push(`${u.turns}t`);
  if (u.input) p.push(`↑${fmtNum(u.input)}`);
  if (u.output) p.push(`↓${fmtNum(u.output)}`);
  if (u.cost) p.push(`$${u.cost.toFixed(3)}`);
  if (u.ctxTokens) p.push(`ctx:${fmtNum(u.ctxTokens)}`);
  if (m) p.push(m);
  return p.join(" ");
}
function statusIcon(s: RunStatus) {
  return s === "pending"
    ? "○"
    : s === "running"
      ? "●"
      : s === "succeeded"
        ? "✓"
        : s === "failed"
          ? "✗"
          : "⊘";
}
function toolIcon(s: ToolStatus) {
  return s === "running" ? "•" : s === "succeeded" ? "✓" : "✗";
}
function latest(text: string, n: number) {
  return text
    .split(/\r?\n/)
    .map((l) => l.trimEnd())
    .filter((l) => l.trim())
    .slice(-n);
}
function appendTail(cur: string, next: string, max: number) {
  if (!next) return cur;
  const c = cur + next;
  return c.length <= max ? c : c.slice(-max);
}
function extractText(content: unknown): string {
  if (typeof content === "string") return content;
  if (!Array.isArray(content)) return "";
  return content
    .map((b) =>
      isObj(b) && b.type === "text" && typeof b.text === "string" ? b.text : "",
    )
    .join("");
}
function extractThinking(content: unknown): string {
  if (!Array.isArray(content)) return "";
  return content
    .map((b) =>
      isObj(b) && b.type === "thinking" && typeof b.thinking === "string"
        ? b.thinking
        : "",
    )
    .join("\n");
}
function newUsage(): Usage {
  return {
    input: 0,
    output: 0,
    cacheRead: 0,
    cacheWrite: 0,
    cost: 0,
    ctxTokens: 0,
    turns: 0,
  };
}
function newRun(
  id: string,
  agent: string,
  prompt: string,
  step?: number,
): RunState {
  return {
    id,
    agent,
    prompt: trunc(prompt.replace(/\s+/g, " ").trim(), 120) || "(empty)",
    step,
    status: "pending",
    finalText: "",
    textTail: "",
    thinkingTail: "",
    stderrTail: "",
    tools: [],
    usage: newUsage(),
  };
}
function cloneProgress(d: ProgressDetails): ProgressDetails {
  return {
    ...d,
    runs: d.runs.map((r) => ({
      ...r,
      tools: r.tools.map((t) => ({ ...t })),
      usage: { ...r.usage },
    })),
  };
}

function oneLine(v: unknown, max = 80) {
  return String(v || "...")
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, max);
}
function summarizeToolArgs(name: string, args: unknown): string {
  const a = isObj(args) ? args : {};
  const summary: JsonObject = {};
  if ("path" in a) summary.path = oneLine(a.path, 240);
  if ("file_path" in a) summary.file_path = oneLine(a.file_path, 240);
  if ("command" in a) summary.command = oneLine(a.command, 240);
  if ("pattern" in a) summary.pattern = oneLine(a.pattern, 120);
  if ("limit" in a) summary.limit = a.limit;
  if ("offset" in a) summary.offset = a.offset;
  if (name === "edit" && Array.isArray(a.edits))
    summary.edits = `${a.edits.length} edit(s)`;
  if (name === "write" && "content" in a)
    summary.content = `<${String(a.content ?? "").length} chars>`;
  const json = JSON.stringify(
    Object.keys(summary).length ? summary : { tool: name },
  );
  return json.length <= MAX_TOOL_ARG_CHARS
    ? json
    : json.slice(0, MAX_TOOL_ARG_CHARS);
}
function toolBrief(t: ToolTrace): string {
  const a = toolArgs(t);
  if (t.name === "read") return `read: ${oneLine(a.path || a.file_path, 80)}`;
  if (t.name === "bash") return `bash: ${oneLine(a.command, 60)}`;
  if (t.name === "write") return `write: ${oneLine(a.path || a.file_path, 80)}`;
  if (t.name === "edit") return `edit: ${oneLine(a.path || a.file_path, 80)}`;
  if (t.name === "grep") return `grep: ${oneLine(a.pattern, 50)}`;
  if (t.name === "find") return `find: ${oneLine(a.pattern || "*", 50)}`;
  return oneLine(t.name, 50);
}

// ── Pi CLI path resolution ────────────────────────────────────────────
const PI_CLI_SEGMENTS = [
  ["node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js"],
  ["node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"],
];

function resolvePiCli(): { command: string; args: string[] } {
  const envCli = str(process.env.TRELLIS_PI_CLI_JS);
  if (envCli) {
    const p = resolve(envCli);
    if (!exists(p)) throw new Error(`TRELLIS_PI_CLI_JS missing: ${p}`);
    return { command: process.execPath, args: [p] };
  }
  const candidates: string[] = [];
  for (const arg of process.argv)
    if (/pi-coding-agent[\\/]dist[\\/]cli\.js$/i.test(arg))
      candidates.push(resolve(arg));
  const prefix =
    str(process.env.npm_config_prefix) ?? str(process.env.NPM_CONFIG_PREFIX);
  const appData = str(process.env.APPDATA);
  const pathVal = process.env.PATH ?? process.env.Path ?? "";
  const addBase = (base: string) => {
    for (const seg of PI_CLI_SEGMENTS) candidates.push(join(base, ...seg));
  };
  if (prefix) {
    addBase(prefix);
    addBase(join(prefix, "lib"));
  }
  if (appData) addBase(join(appData, "npm"));
  for (const entry of pathVal.split(delimiter)) {
    const e = entry.trim();
    if (!e) continue;
    addBase(e);
    addBase(dirname(e));
    addBase(join(dirname(e), "lib"));
  }
  for (const c of [...new Set(candidates)])
    if (exists(c)) return { command: process.execPath, args: [c] };
  return { command: "pi", args: [] };
}

function resolveRunCfg(
  input: SubagentInput,
  agentCfg: AgentConfig,
  inheritedThinking?: string,
): PiRunConfig {
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
  const normalize = (v: unknown): string | undefined => {
    const s = typeof v === "string" && v.trim() ? v.trim().toLowerCase() : "";
    return THINKING_LEVELS.includes(s) ? s : undefined;
  };
  const suffixRe = /:(off|minimal|low|medium|high|xhigh)$/i;
  const inputModel = str(input.model);
  const agentModel = agentCfg.model;
  const rawModel = inputModel ?? agentModel;
  const inputSuffixThinking = normalize(inputModel?.match(suffixRe)?.[1]);
  const agentSuffixThinking = normalize(agentModel?.match(suffixRe)?.[1]);
  const baseModel = rawModel?.replace(suffixRe, "");
  const thinking =
    normalize(input.thinking) ??
    inputSuffixThinking ??
    normalize(agentCfg.thinking) ??
    agentSuffixThinking ??
    normalize(inheritedThinking);
  if (baseModel && thinking && thinking !== "off")
    return { model: `${baseModel}:${thinking}`, thinking, tools: agentCfg.tools };
  return { model: baseModel || rawModel, thinking, tools: agentCfg.tools };
}

function buildPiArgs(cfg: PiRunConfig): string[] {
  const args = ["--mode", "json", "-p", "--no-session"];
  if (cfg.model)
    args.push(
      "--model",
      cfg.thinking && cfg.thinking !== "off" && !cfg.model.includes(":")
        ? `${cfg.model}:${cfg.thinking}`
        : cfg.model,
    );
  else if (cfg.thinking && cfg.thinking !== "off")
    args.push("--thinking", cfg.thinking);
  if (cfg.tools && cfg.tools.length > 0)
    args.push("--tools", cfg.tools.join(","));
  return args;
}

// ── BoundedBufferCollector ─────────────────────────────────────────────
class BBC {
  private c: Buffer[] = [];
  private len = 0;
  private trunc = 0;
  constructor(private max: number) {}
  append(b: Buffer) {
    if (b.length >= this.max) {
      this.trunc += this.len + b.length - this.max;
      this.c = [b.subarray(b.length - this.max)];
      this.len = this.max;
      return;
    }
    this.c.push(b);
    this.len += b.length;
    while (this.len > this.max) {
      const f = this.c[0]!;
      if (f.length <= this.len - this.max) {
        this.c.shift();
        this.len -= f.length;
        this.trunc += f.length;
      } else {
        const ov = this.len - this.max;
        this.c[0] = f.subarray(ov);
        this.len -= ov;
        this.trunc += ov;
        break;
      }
    }
  }
  toString() {
    const body = Buffer.concat(this.c, this.len).toString("utf-8");
    return this.trunc ? `[${this.trunc} bytes truncated]\n${body}` : body;
  }
}

// ── Trellis Context ────────────────────────────────────────────────────
function findRoot(start: string): string {
  let c = resolve(start);
  while (true) {
    if (existsSync(join(c, ".trellis")) || existsSync(join(c, ".pi"))) return c;
    const p = dirname(c);
    if (p === c) return resolve(start);
    c = p;
  }
}
function splitFM(c: string) {
  const m = c.replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
  return m
    ? { fm: m[1] ?? "", body: c.slice(m[0].length) }
    : { fm: "", body: c };
}
function stripFM(c: string) {
  return splitFM(c).body.trimStart();
}
function parseAgentFM(c: string): AgentConfig {
  const cfg: AgentConfig = { fallbackModels: [] };
  const { fm } = splitFM(c);
  const lines = fm.split(/\r?\n/);
  for (let i = 0; i < lines.length; i++) {
    const m = (lines[i] ?? "").match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
    if (!m) continue;
    const k = m[1] ?? "",
      v = m[2] ?? "";
    if (k === "model")
      cfg.model = v.trim().replace(/^["']|["']$/g, "") || undefined;
    else if (k === "thinking")
      cfg.thinking = (v.trim().replace(/^["']|["']$/g, "") || undefined) as
        | string
        | undefined;
    else if (k === "fallbackModels" || k === "fallback_models") {
      if (v.trim()) {
        cfg.fallbackModels = v
          .trim()
          .replace(/^\[|\]$/g, "")
          .split(",")
          .map((s) => s.trim().replace(/^["']|["']$/g, ""))
          .filter(Boolean);
      } else {
        i++;
        while (i < lines.length && /^\s+-\s/.test(lines[i] ?? "")) {
          const item = (lines[i] ?? "")
            .trim()
            .replace(/^-\s+/, "")
            .replace(/^["']|["']$/g, "");
          if (item) cfg.fallbackModels.push(item);
          i++;
        }
        i--;
      }
    } else if (k === "tools") {
      // Pi tool names are lowercase (read, bash, edit, write, grep, find, ls).
      // Normalize to lowercase so mixed-case frontmatter still matches.
      if (v.trim()) {
        cfg.tools = v
          .trim()
          .split(",")
          .map((s) => s.trim().replace(/^["']|["']$/g, "").toLowerCase())
          .filter(Boolean);
      }
    }
  }
  return cfg;
}

function contextKey(input?: unknown, ctx?: PiExtensionContext): string | null {
  const ov = str(process.env.TRELLIS_CONTEXT_ID);
  if (ov) return ov.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 160) || hash(ov);
  const sessionId =
    callStr(ctx?.sessionManager?.getSessionId) ??
    str(process.env.PI_SESSION_ID) ??
    str(process.env.PI_SESSIONID) ??
    lookupStr(input, ["session_id", "sessionId", "sessionID"]);
  if (sessionId)
    return `pi_${sessionId.replace(/[^A-Za-z0-9._-]+/g, "_") || hash(sessionId)}`;
  const transcriptPath =
    callStr(ctx?.sessionManager?.getSessionFile) ??
    lookupStr(input, ["transcript_path", "transcriptPath", "transcript"]);
  if (transcriptPath) return `pi_transcript_${hash(transcriptPath)}`;
  return null;
}

function readTaskDir(root: string, key: string | null): string | null {
  if (!key) return null;
  try {
    const ctx = JSON.parse(
      readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)),
    ) as JsonObject;
    let ref = str(ctx.current_task);
    if (!ref) return null;
    ref = ref;
    ref = ref.replace(/\\/g, "/").replace(/^\.\//, "");
    if (ref.startsWith("tasks/")) ref = `.trellis/${ref}`;
    return ref.startsWith(".trellis/")
      ? join(root, ref)
      : isAbsolute(ref)
        ? ref
        : join(root, ".trellis", "tasks", ref);
  } catch {
    return null;
  }
}
function sessionHasTask(root: string, key: string): boolean {
  try {
    const ctx = JSON.parse(
      readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)),
    ) as JsonObject;
    return !!str(ctx.current_task);
  } catch {
    return false;
  }
}
function adoptKey(root: string, key: string): string {
  if (sessionHasTask(root, key)) return key;
  try {
    const dir = join(root, ".trellis", ".runtime", "sessions");
    const keys = readdirSync(dir)
      .filter(
        (f) => f.endsWith(".json") && sessionHasTask(root, f.slice(0, -5)),
      )
      .map((f) => f.slice(0, -5));
    const proc = keys.filter((k) => k.startsWith("pi_process_"));
    const cands = proc.length ? proc : keys;
    return cands.length === 1 ? cands[0]! : key;
  } catch {
    return key;
  }
}

// ── Workflow State Breadcrumb ─────────────────────────────────────────
const WF_RE =
  /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
function workflowBreadcrumb(root: string, key: string | null): string {
  const wf = readText(join(root, ".trellis", "workflow.md"));
  if (!wf) return "";
  const templates: Record<string, string> = {};
  for (const m of wf.matchAll(WF_RE)) {
    const s = m[1] ?? "",
      b = (m[2] ?? "").trim();
    if (s && b) templates[s] = b;
  }
  const dir = readTaskDir(root, key);
  let header = "Status: no_task",
    lookup = "no_task";
  if (dir) {
    try {
      const d = JSON.parse(readText(join(dir, "task.json"))) as JsonObject;
      const status = str(d.status) ?? "";
      const id = str(d.id) ?? dir.split(/[\\/]/).pop() ?? "";
      if (status) {
        header = `Task: ${id} (${status})`;
        lookup = status;
      }
    } catch {}
  }
  const body = templates[lookup] ?? "Refer to workflow.md for current step.";
  return `<workflow-state>\n${header}\n${body}\n</workflow-state>`;
}

// ── Session Overview ───────────────────────────────────────────────────
function runContextScript(root: string, key: string | null, args: string[]): string {
  const script = join(root, ".trellis", "scripts", "get_context.py");
  if (!exists(script)) return "";
  try {
    const py = process.platform === "win32" ? "python" : "python3";
    const result = spawnSync(py, [script, ...args], {
      cwd: root,
      env: key ? { ...process.env, TRELLIS_CONTEXT_ID: key } : process.env,
      encoding: "utf-8",
      timeout: SESSION_OVERVIEW_TIMEOUT_MS,
      windowsHide: true,
    });
    if (result.status !== 0) return "";
    const stdout = (result.stdout ?? "").trim();
    return stdout;
  } catch {
    return "";
  }
}

function sessionOverview(root: string, key: string | null): string {
  const stdout = runContextScript(root, key, []);
  return stdout ? `<session-overview>\n${stdout}\n</session-overview>` : "";
}

function workflowOverview(root: string, key: string | null): string {
  const stdout = runContextScript(root, key, [
    "--mode",
    "phase",
    "--platform",
    "pi",
  ]);
  return stdout ? `<trellis-workflow>\n${stdout}\n</trellis-workflow>` : "";
}

function buildStartupContext(
  root: string,
  key: string | null,
  overview: string,
): string {
  const workflow = workflowOverview(root, key);
  return [
    "<session-context>\nTrellis compact SessionStart context. Use it to orient the session; load details on demand.\n</session-context>",
    FIRST_REPLY_NOTICE,
    overview,
    workflow,
    "<ready>\nUse the current workflow state to decide whether to create, continue, or skip a Trellis task.\n</ready>",
  ]
    .filter(Boolean)
    .join("\n\n");
}

function buildContext(root: string, agent: string, key: string | null): string {
  const dir = readTaskDir(root, key);
  if (!dir)
    return "No active Trellis task found. Read .trellis/ before proceeding.";
  const prd = readText(join(dir, "prd.md"));
  const design = readText(join(dir, "design.md"));
  const impl = readText(join(dir, "implement.md"));
  const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? "";
  let spec = "";
  if (jsonlName) {
    const chunks: string[] = [];
    for (const line of readText(join(dir, jsonlName)).split(/\r?\n/)) {
      const t = line.trim();
      if (!t) continue;
      try {
        const r = JSON.parse(t) as JsonObject;
        const f = typeof r.file === "string" ? r.file : "";
        if (f) {
          const c = readText(join(root, f));
          if (c) chunks.push(`## ${f}\n\n${c}`);
        }
      } catch {}
    }
    spec = chunks.join("\n\n---\n\n");
  }
  return [
    `## Trellis Task Context`,
    `Task directory: ${dir}`,
    "",
    "### prd.md",
    prd || "(missing)",
    design ? "\n### design.md\n" + design : "",
    impl ? "\n### implement.md\n" + impl : "",
    spec ? "\n### Curated Spec / Research Context\n" + spec : "",
  ].join("\n");
}

function normalizeAgent(agent: string | undefined): string {
  const name = agent ?? "trellis-implement";
  return name.startsWith("trellis-") ? name : `trellis-${name}`;
}

function isTrellisAgent(root: string, agent: string): boolean {
  return existsSync(join(root, ".pi", "agents", `${agent}.md`));
}

function buildPrompt(
  root: string,
  input: SubagentInput,
  key: string | null,
): string {
  const agent = normalizeAgent(input.agent);
  const raw = readText(join(root, ".pi", "agents", `${agent}.md`));
  const def = stripFM(raw);
  const ctx = buildContext(root, agent, key);
  return [
    "## Trellis Agent Definition",
    def || "(missing)",
    "",
    ctx,
    "",
    "## Delegated Task",
    input.prompt ?? "",
  ].join("\n");
}

// ── Event parsing ─────────────────────────────────────────────────────
function parseJsonEvent(line: string): JsonObject | null {
  const t = line.trim();
  if (!t) return null;
  const i = t.indexOf("{");
  if (i < 0) return null;
  try {
    const p = JSON.parse(t.slice(i));
    return isObj(p) ? p : null;
  } catch {
    return null;
  }
}

function applyEvent(r: RunState, evt: JsonObject): boolean {
  const type = typeof evt.type === "string" ? evt.type : "";
  if (!type) return false;
  if (type === "agent_start" || type === "turn_start") {
    r.status = "running";
    r.startedAt ??= Date.now();
    return true;
  }
  if (type === "message_update") {
    const ae = isObj(evt.assistantMessageEvent)
      ? evt.assistantMessageEvent
      : null;
    if (!ae || typeof ae.delta !== "string") return false;
    if (ae.type === "thinking_delta") {
      r.thinkingTail = appendTail(r.thinkingTail, ae.delta, MAX_TAIL);
      return true;
    }
    if (ae.type === "text_delta") {
      r.textTail = appendTail(r.textTail, ae.delta, MAX_TAIL);
      return true;
    }
    return false;
  }
  if (type === "message_end" && isObj(evt.message)) {
    const msg = evt.message;
    if (msg.role !== "assistant") return false;
    r.usage.turns += 1;
    const u = isObj(msg.usage) ? msg.usage : null;
    const cost = isObj(u?.cost) ? u.cost : null;
    r.usage.input += num(u?.input);
    r.usage.output += num(u?.output);
    r.usage.cacheRead += num(u?.cacheRead);
    r.usage.cacheWrite += num(u?.cacheWrite);
    r.usage.cost += num(cost?.total);
    r.usage.ctxTokens = num(u?.totalTokens);
    const thinking = extractThinking(msg.content);
    if (thinking) r.thinkingTail = appendTail("", thinking, MAX_TAIL);
    const text = extractText(msg.content);
    if (text) {
      r.finalText = text;
      r.textTail = appendTail("", text, MAX_TAIL);
    }
    if (typeof msg.model === "string") {
      const parsed = splitModelThinking(msg.model, r.thinking);
      r.model = parsed.model;
      r.thinking = parsed.thinking;
    }
    if (typeof msg.errorMessage === "string") r.errorMessage = msg.errorMessage;
    return true;
  }
  if (type === "tool_execution_start") {
    const id =
      typeof evt.toolCallId === "string"
        ? evt.toolCallId
        : hash(`${Date.now()}`);
    const name = typeof evt.toolName === "string" ? evt.toolName : "tool";
    const args = summarizeToolArgs(name, evt.args);
    const existing = r.tools.findIndex((t) => t.id === id);
    if (existing >= 0)
      r.tools[existing] = { ...r.tools[existing]!, args, status: "running" };
    else
      r.tools.push({
        id,
        name,
        args,
        status: "running",
        startedAt: Date.now(),
      });
    if (r.tools.length > MAX_TOOLS)
      r.tools.splice(0, r.tools.length - MAX_TOOLS);
    return true;
  }
  if (type === "tool_execution_end") {
    const id = typeof evt.toolCallId === "string" ? evt.toolCallId : "";
    const idx = r.tools.findIndex((t) => t.id === id);
    if (idx >= 0)
      r.tools[idx] = {
        ...r.tools[idx]!,
        status: evt.isError ? "failed" : "succeeded",
        finishedAt: Date.now(),
      };
    return true;
  }
  if (type === "agent_end") {
    r.finishedAt = Date.now();
    if (r.status === "running" || r.status === "pending")
      r.status = "succeeded";
    return true;
  }
  return false;
}

function finalize(r: RunState, fallback: string): string {
  return r.finalText || fallback.trim() || r.stderrTail.trim();
}
function formatPiOutput(stdout: string, stderr: string): string {
  let ft = "";
  for (const line of stdout.split(/\r?\n/)) {
    const t = line.trim();
    if (!t) continue;
    try {
      const evt = JSON.parse(t) as JsonObject;
      const msg = isObj(evt.message) ? evt.message : null;
      if (msg?.role === "assistant") {
        const txt = extractText(msg.content);
        if (txt) ft = txt;
      }
    } catch {}
  }
  return ft || stdout || stderr;
}

// ── runPi: subprocess execution + event processing ───────────────────
function runPi(
  root: string,
  prompt: string,
  cfg: PiRunConfig,
  state: RunState,
  emit: () => void,
  key?: string | null,
  signal?: AbortSignal,
): Promise<{ output: string; failed: boolean }> {
  return new Promise((resolve) => {
    if (signal?.aborted) {
      state.status = "cancelled";
      state.errorMessage = "cancelled";
      state.finishedAt = Date.now();
      emit();
      resolve({ output: "cancelled", failed: true });
      return;
    }
    const inv = resolvePiCli();
    const childEnv = {
      ...process.env,
      TRELLIS_SUBAGENT_CHILD: "1",
      ...(key ? { TRELLIS_CONTEXT_ID: key } : {}),
    };
    const cli = spawn(inv.command, [...inv.args, ...buildPiArgs(cfg)], {
      cwd: root,
      env: childEnv,
      stdio: ["pipe", "pipe", "pipe"],
      windowsHide: true,
    });
    const stdout = new BBC(MAX_STDOUT);
    const stderr = new BBC(MAX_STDERR);
    let buf = "";
    let settled = false;
    let aborted = false;
    let killTimer: ReturnType<typeof setTimeout> | null = null;
    const abort = () => {
      aborted = true;
      cli.kill();
      killTimer = setTimeout(() => {
        if (!settled && cli.exitCode === null) cli.kill("SIGKILL");
      }, ABORT_KILL_GRACE_MS);
      killTimer?.unref?.();
    };
    const done = (v: { output: string; failed: boolean }) => {
      if (settled) return;
      settled = true;
      if (killTimer) clearTimeout(killTimer);
      signal?.removeEventListener("abort", abort);
      emit();
      resolve(v);
    };
    signal?.addEventListener("abort", abort, { once: true });
    state.status = "running";
    state.startedAt = Date.now();
    emit();
    const processLine = (line: string) => {
      const evt = parseJsonEvent(line);
      if (evt && applyEvent(state, evt)) emit();
    };
    cli.stdout?.on("data", (d: Buffer) => {
      stdout.append(d);
      buf += d.toString("utf-8");
      if (buf.length > MAX_LINE_BUFFER) buf = buf.slice(-MAX_LINE_BUFFER);
      const lines = buf.split(/\r?\n/);
      buf = lines.pop() ?? "";
      for (const l of lines) processLine(l);
    });
    cli.stderr?.on("data", (d: Buffer) => {
      stderr.append(d);
      state.stderrTail = appendTail(
        state.stderrTail,
        d.toString("utf-8"),
        MAX_TAIL,
      );
    });
    cli.stdin?.on("error", (e: Error & { code?: string }) => {
      if (!aborted && e.code !== "EPIPE")
        done({ output: e.message, failed: true });
    });
    cli.on("error", (e) => {
      state.status = aborted ? "cancelled" : "failed";
      state.errorMessage = e instanceof Error ? e.message : String(e);
      state.finishedAt = Date.now();
      done({ output: finalize(state, state.errorMessage), failed: true });
    });
    cli.on("close", (code) => {
      if (buf.trim()) processLine(buf);
      const out = stdout.toString();
      const err = stderr.toString();
      state.stderrTail = appendTail("", err, MAX_TAIL);
      state.finishedAt = Date.now();
      if (aborted) {
        state.status = "cancelled";
        state.errorMessage = "cancelled";
        done({ output: finalize(state, "cancelled"), failed: true });
        return;
      }
      if (code === 0) {
        if (state.status === "pending" || state.status === "running")
          state.status = "succeeded";
        done({
          output: finalize(state, formatPiOutput(out, err)),
          failed: false,
        });
        return;
      }
      state.status = "failed";
      state.errorMessage = err || out || `exit ${code ?? "?"}`;
      done({ output: finalize(state, state.errorMessage), failed: true });
    });
    cli.stdin?.end(prompt);
  });
}

// ── runSubagent: orchestrate single/parallel/chain via native partial updates ──
async function runSubagent(
  root: string,
  input: SubagentInput,
  key: string | null,
  signal?: AbortSignal,
  onUpdate?: (r: PiToolResult) => void,
  inheritedThinking?: string,
): Promise<{ output: string; details: ProgressDetails; failed: boolean }> {
  const agentName = normalizeAgent(input.agent);
  const agentRaw = readText(join(root, ".pi", "agents", `${agentName}.md`));
  const agentCfg = parseAgentFM(agentRaw);
  const runCfg = resolveRunCfg(input, agentCfg, inheritedThinking);
  const mode = input.mode ?? "single";
  const startedAt = Date.now();
  const details: ProgressDetails = {
    kind: "trellis-subagent-progress",
    agent: agentName,
    mode,
    startedAt,
    updatedAt: startedAt,
    final: false,
    runs: [],
  };
  let lastEmit = 0;
  let lastPartialKey = "";
  let closed = false;
  const pushPartial = (force = false) => {
    if (closed || !onUpdate) return;
    const key = progressKey(details);
    if (!force && key === lastPartialKey) return;
    lastPartialKey = key;
    onUpdate({
      // Keep native partial content stable; renderResult owns the visible progress UI.
      content: [{ type: "text", text: "subagent running" }],
      details: cloneProgress(details),
    });
  };
  const emit = (force = false) => {
    const now = Date.now();
    if (!force && now - lastEmit < THROTTLE_MS) return;
    lastEmit = now;
    details.updatedAt = now;
    pushPartial(force);
  };
  const finish = (output: string, failed: boolean) => {
    closed = true;
    details.final = true;
    details.updatedAt = Date.now();
    return { output, details: cloneProgress(details), failed };
  };

  try {
    if (mode === "parallel") {
      const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
      details.runs = prompts.map((p, i) => {
        const r = newRun(`${agentName}-${i + 1}`, agentName, p);
        applyRunConfig(r, runCfg);
        return r;
      });
      emit(true);
      const results = await Promise.all(
        prompts.map((p, i) =>
          runPi(
            root,
            buildPrompt(root, { ...input, prompt: p }, key),
            runCfg,
            details.runs[i]!,
            emit,
            key,
            signal,
          ),
        ),
      );
      return finish(
        results.map((r) => r.output).join("\n\n---\n\n"),
        results.some((r) => r.failed),
      );
    }
    if (mode === "chain") {
      let prev = "";
      let failed = false;
      for (let i = 0; i < (input.prompts?.length ?? 1); i++) {
        const p = input.prompts?.[i] ?? input.prompt ?? "";
        const rs = newRun(`${agentName}-${i + 1}`, agentName, p, i + 1);
        applyRunConfig(rs, runCfg);
        details.runs.push(rs);
        emit(true);
        const result = await runPi(
          root,
          buildPrompt(
            root,
            {
              ...input,
              prompt: prev ? `${p}\n\nPrevious output:\n${prev}` : p,
            },
            key,
          ),
          runCfg,
          rs,
          emit,
          key,
          signal,
        );
        prev = result.output;
        failed = failed || result.failed;
        if (result.failed) break;
      }
      return finish(prev, failed);
    }
    const rs = newRun(`${agentName}-1`, agentName, input.prompt ?? "");
    applyRunConfig(rs, runCfg);
    details.runs = [rs];
    emit(true);
    const result = await runPi(
      root,
      buildPrompt(root, input, key),
      runCfg,
      rs,
      emit,
      key,
      signal,
    );
    return finish(result.output, result.failed);
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    const r = activeRun(details);
    if (r) {
      r.status = "failed";
      r.errorMessage = message;
      r.finishedAt = Date.now();
    }
    return finish(message, true);
  }
}

// ── Extension ──────────────────────────────────────────────────────────
export default function trellisExtension(pi: {
  registerTool?: (tool: JsonObject) => void;
  registerShortcut?: (
    key: string,
    opts: {
      description?: string;
      handler: (ctx: PiExtensionContext) => unknown;
    },
  ) => void;
  on?: (
    event: string,
    handler: (event: unknown, ctx?: PiExtensionContext) => unknown,
  ) => void;
  getThinkingLevel?: () => string;
}): void {
  if (process.env.TRELLIS_SUBAGENT_CHILD === "1") return;
  const root = findRoot(process.cwd());
  const procKey = `pi_process_${hash([root, process.pid, Date.now(), randomBytes(8).toString("hex")].join(":"))}`;
  let curKey: string | null = null;

  const getKey = (input?: unknown, ctx?: PiExtensionContext) => {
    const k = adoptKey(root, contextKey(input, ctx) ?? curKey ?? procKey);
    curKey = k;
    return k;
  };

  // Per-turn cache to avoid double-spawning python
  let turnCache: {
    key: string | null;
    ts: number;
    wf: string;
    ov: string;
  } | null = null;
  const getTurnCtx = (k: string | null) => {
    const now = Date.now();
    if (turnCache && turnCache.key === k && now - turnCache.ts < 1500)
      return turnCache;
    turnCache = {
      key: k,
      ts: now,
      wf: workflowBreadcrumb(root, k),
      ov: sessionOverview(root, k),
    };
    return turnCache;
  };
  const startupKeys = new Set<string>();
  const getStartupCtx = (
    k: string | null,
    turn: { ov: string },
  ): string => {
    const key = k ?? "default";
    if (startupKeys.has(key)) return "";
    startupKeys.add(key);
    return buildStartupContext(root, k, turn.ov);
  };

  // Toggle only the latest subagent native card; do not use Pi global tool expansion.
  const toggleDetail = (ctx: PiExtensionContext) => {
    const id = activeSubagentToolCallId;
    const card = id ? nativeCards.get(id) : undefined;
    if (!card) {
      ctx.ui?.notify?.("No subagent card to toggle yet.", "warning");
      return;
    }
    card.state.localExpanded = card.state.localExpanded !== true;
    card.invalidate();
  };

  pi.registerShortcut?.("alt+o", {
    description: "Toggle latest subagent card details",
    handler: async (ctx: PiExtensionContext) => toggleDetail(ctx),
  });

  // Tool registration
  pi.registerTool?.({
    name: "trellis_subagent",
    label: "Trellis Subagent",
    description: "Run a Trellis project sub-agent with active task context.",
    promptSnippet:
      'Sub-agent dispatch protocol (Trellis): your dispatch prompt MUST start with one line "Active task: <task path from `task.py current`>" before any other instructions.',
    promptGuidelines: [
      'Use subagent for task delegation. Your dispatch prompt MUST start with "Active task: <task path from `task.py current`>".',
    ],
    parameters: {
      type: "object",
      properties: {
        agent: {
          type: "string",
          description:
            "Agent name, such as trellis-implement or trellis-check.",
        },
        prompt: {
          type: "string",
          description: "Task prompt for the sub-agent.",
        },
        mode: { type: "string", enum: ["single", "parallel", "chain"] },
        prompts: {
          type: "array",
          items: { type: "string" },
          maxItems: MAX_PARALLEL_PROMPTS,
        },
        model: {
          type: "string",
          description:
            "Optional Pi model override for the child sub-agent process.",
        },
        thinking: {
          type: "string",
          description:
            "Optional Pi thinking level override for the child sub-agent process.",
          enum: ["off", "minimal", "low", "medium", "high", "xhigh"],
        },
      },
    },
    execute: async (
      id: string,
      input: SubagentInput,
      signal?: AbortSignal,
      onUpdate?: (r: PiToolResult) => void,
      ctx?: PiExtensionContext,
    ) => {
      activeSubagentToolCallId = id;
      const agentName = normalizeAgent(input.agent);
      if (!isTrellisAgent(root, agentName)) {
        return {
          content: [
            {
              type: "text",
              text:
                "`trellis_subagent` is only for Trellis workflow agents with a definition file in .pi/agents/.\n\n" +
                `No definition found for: ${agentName}\n\n` +
                "For general-purpose sub-agents, use one of these community tools:\n" +
                "- `subagent` tool from npm:pi-subagents (nicobailon/pi-subagents)\n" +
                "- `Agent` tool from npm:@tintinweb/pi-subagents\n\n" +
                "If neither is installed, ask the user to either:\n" +
                `- Create .pi/agents/${agentName}.md for your custom Trellis agent\n` +
                "- Install a community subagent package: pi install -l npm:@tintinweb/pi-subagents",
            },
          ],
          details: { agent: agentName, error: "not a trellis workflow agent" },
        };
      }
      const mode = input.mode ?? "single";
      const prompt = input.prompt?.trim();
      const prompts = input.prompts?.map((p) => p.trim()).filter(Boolean);
      if (mode === "single" && !prompt)
        throw new Error("subagent prompt is required for single mode");
      if (
        (mode === "parallel" || mode === "chain") &&
        !prompt &&
        !prompts?.length
      )
        throw new Error(
          "subagent prompt or prompts are required for parallel/chain mode",
        );
      if (
        mode === "parallel" &&
        prompts &&
        prompts.length > MAX_PARALLEL_PROMPTS
      )
        throw new Error(
          `subagent parallel mode supports at most ${MAX_PARALLEL_PROMPTS} prompts`,
        );
      const cleanInput: SubagentInput = {
        ...input,
        prompt,
        prompts: prompts?.length ? prompts : undefined,
      };
      const key = getKey(cleanInput, ctx);
      const inheritedThinking = pi.getThinkingLevel?.();
      const result = await runSubagent(
        root,
        cleanInput,
        key,
        signal,
        onUpdate,
        inheritedThinking,
      );
      return {
        content: [{ type: "text", text: result.output }],
        details: result.details,
      };
    },
    // Hide the call renderer so the native card only shows result/progress once.
    renderCall: () => ({
      render() {
        return [];
      },
      invalidate() {},
    }),
    renderResult: (
      result: PiToolResult,
      _opts?: { expanded?: boolean; isPartial?: boolean },
      _theme?: unknown,
      context?: unknown,
    ) => {
      const ctxObj = isObj(context) ? context : null;
      const toolCallId = str(ctxObj?.toolCallId);
      const state = isObj(ctxObj?.state) ? (ctxObj.state as JsonObject) : null;
      const invalidate =
        typeof ctxObj?.invalidate === "function"
          ? (ctxObj.invalidate as () => void)
          : null;
      const isProgress =
        isObj(result.details) &&
        result.details.kind === "trellis-subagent-progress";
      if (toolCallId && state && invalidate) {
        const updatedAt = isProgress
          ? (result.details as ProgressDetails).updatedAt
          : Date.now();
        rememberNativeCard(toolCallId, { state, invalidate, updatedAt });
      }
      return {
        render(w: number) {
          if (isProgress) {
            const expanded = state?.localExpanded === true;
            return renderProgressCard(
              result.details as ProgressDetails,
              expanded,
              w,
            );
          }
          return [trunc(result.content?.[0]?.text ?? "(no output)", w)];
        },
        invalidate() {},
      };
    },
  });

  // Events
  pi.on?.("session_start", (event, ctx) => {
    getKey(event, ctx);
    ctx?.ui?.notify?.(
      "Trellis project context is available. Use /trellis-start to bootstrap or /trellis-continue to resume.",
      "info",
    );
  });
  pi.on?.("session_shutdown", () => {
    nativeCards.clear();
    activeSubagentToolCallId = null;
  });
  pi.on?.("tool_call", (event, ctx) => {
    const k = getKey(event, ctx);
    const ev = event as { toolName?: string; input?: JsonObject };
    if (
      ev.toolName === "bash" &&
      isObj(ev.input) &&
      typeof ev.input.command === "string" &&
      !cmdHasTrellisCtx(ev.input.command)
    )
      ev.input.command = `export TRELLIS_CONTEXT_ID=${shellQuote(k)}; ${ev.input.command}`;
  });
  // Preserve progress details from execute(); mark failed subagent results through
  // the official tool_result patch hook instead of throwing away renderer details.
  pi.on?.("tool_result", (event) => {
    const ev = event as { toolName?: string; details?: unknown };
    if (
      ev.toolName === "trellis_subagent" &&
      isObj(ev.details) &&
      ev.details.kind === "trellis-subagent-progress" &&
      Array.isArray(ev.details.runs) &&
      ev.details.runs.some(
        (r) => isObj(r) && (r.status === "failed" || r.status === "cancelled"),
      )
    )
      return { isError: true };
    return undefined;
  });
  pi.on?.("input", (event, ctx) => {
    const k = getKey(event, ctx);
    const ev = event as { text?: string };
    if (typeof ev.text !== "string" || !ev.text.trim())
      return { action: "continue" };
    const { wf, ov } = getTurnCtx(k);
    const injection = [wf, ov].filter(Boolean).join("\n\n");
    if (!injection) return { action: "continue" };
    return {
      action: "transform",
      text: [ev.text, injection].join("\n\n"),
    };
  });
  pi.on?.("before_agent_start", (event, ctx) => {
    const k = getKey(event, ctx);
    const cur = (event as { systemPrompt?: string }).systemPrompt ?? "";
    const ctxText = buildContext(root, "trellis-implement", k);
    const turn = getTurnCtx(k);
    const startup = getStartupCtx(k, turn);
    return {
      systemPrompt: [cur, startup, ctxText, turn.wf, turn.ov]
        .filter(Boolean)
        .join("\n\n"),
    };
  });
  pi.on?.("context", (event, ctx) => {
    getKey(event, ctx);
  });
}
