// Live assembly line — full app, identical UX to the original AI-Studio app.

const { useState: useStateLive, useEffect: useEffectLive } = React;

const TaskHistoryRow = ({ task, station, onOpen }) => {
  const [expanded, setExpanded] = useStateLive(false);
  const events = task.events || [];
  const [selectedVersion, setSelectedVersion] = useStateLive(events.length - 1);
  // Backwards-compat shim: synthesize a flat history string list from events.
  const history = events.map((e) => e.message);

  useEffectLive(() => {
    if (expanded) setSelectedVersion(events.length - 1);
  }, [expanded, events.length]);

  // Reuse `task.history`-style references below by aliasing.
  task = { ...task, history };

  return (
    <div className="bg-white border-[2px] border-neutral-200/60 rounded-2xl shadow-sm overflow-hidden flex flex-col transition-shadow hover:shadow-md">
      <div
        className="p-6 flex items-center justify-between cursor-pointer hover:bg-neutral-50/80 transition-colors"
        onClick={() => setExpanded(!expanded)}
      >
        <div className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6">
          <div className={`w-12 h-12 shrink-0 rounded-2xl flex items-center justify-center font-bold shadow-inner ${expanded ? "bg-indigo-50 text-indigo-700 border border-indigo-100" : "bg-neutral-100 text-neutral-500 border border-neutral-200"}`}>
            <FileText size={24} className={expanded ? "text-indigo-600" : "text-neutral-400"} />
          </div>
          <div>
            <div className="flex flex-wrap items-center gap-2 mb-1.5">
              <span className="text-xs font-mono font-semibold text-neutral-500 bg-neutral-100 border border-neutral-200 px-2 py-0.5 rounded-md leading-none">
                {task.id}
              </span>
              <span className={`text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-md leading-none ${
                task.status === "working" ? "bg-sky-100 text-sky-700 border border-sky-200/50" :
                task.status === "validating" ? "bg-amber-100 text-amber-700 border border-amber-200/50" :
                task.status === "rejected" ? "bg-red-100 text-red-700 border border-red-200/50" :
                "bg-neutral-100 text-neutral-600 border border-neutral-200/50"
              }`}>
                {task.status}
              </span>
            </div>
            <h3 className="font-bold text-neutral-900 text-xl leading-snug">{task.title}</h3>
          </div>
        </div>
        <div className="flex items-center gap-6 shrink-0">
          <div className="hidden md:flex flex-col items-end">
            <div className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest">Timeline</div>
            <div className="text-sm font-semibold text-neutral-700">{task.history.length} Versions</div>
          </div>
          <div className={`p-2 rounded-full transition-colors ${expanded ? "bg-indigo-100 text-indigo-700" : "bg-neutral-100 text-neutral-500"}`}>
            {expanded ? <ChevronDown size={20} /> : <ChevronRight size={20} />}
          </div>
        </div>
      </div>

      {expanded && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          className="border-t border-neutral-100 bg-neutral-50/50"
        >
          <div className="p-8 flex flex-col lg:flex-row gap-10 items-start">
            <div className="w-full lg:w-[320px] shrink-0 flex flex-col gap-3 relative before:absolute before:inset-y-0 before:left-[21px] before:w-0.5 before:bg-neutral-200">
              <div className="pl-12 pb-2 text-[10px] font-bold uppercase tracking-widest text-neutral-400">
                Version History
              </div>
              {task.history.map((log, idx) => {
                const isSelected = selectedVersion === idx;
                return (
                  <button
                    key={idx}
                    onClick={() => setSelectedVersion(idx)}
                    className={`relative flex items-start gap-5 text-left p-4 rounded-xl transition-all ${isSelected ? "bg-white shadow-md border border-neutral-200 z-10 scale-100" : "hover:bg-neutral-100 opacity-60 hover:opacity-100 z-0 scale-[0.98]"}`}
                  >
                    <div className={`shrink-0 w-5 h-5 mt-0.5 rounded-full border-[3px] ring-4 ring-neutral-50/50 transition-colors ${isSelected ? "bg-indigo-600 border-white shadow-sm" : "bg-white border-neutral-300"}`} />
                    <div className="flex flex-col gap-1">
                      <span className={`text-[11px] font-bold uppercase tracking-widest ${isSelected ? "text-indigo-600" : "text-neutral-500"}`}>
                        Version {idx + 1}
                        {idx === task.history.length - 1 && <span className="ml-2 bg-indigo-100 text-indigo-700 px-1.5 py-0.5 rounded text-[9px]">LATEST</span>}
                      </span>
                      <span className={`text-sm leading-snug ${isSelected ? "text-neutral-900 font-semibold" : "text-neutral-600 font-medium"}`}>{log}</span>
                    </div>
                  </button>
                );
              })}
            </div>

            <div className="flex-1 w-full bg-white border-[2px] border-indigo-100 rounded-2xl shadow-xl p-8 relative overflow-hidden">
              <div className="absolute top-0 right-0 bg-indigo-50 text-indigo-700 text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-bl-xl border-b border-l border-indigo-100">
                Viewing v{selectedVersion + 1} Snapshot
              </div>
              <div className="flex items-center gap-2.5 mb-6">
                <GitCommit className="text-indigo-400" size={20} />
                <h4 className="text-sm font-bold text-indigo-900 uppercase tracking-widest">Snapshot Details</h4>
              </div>
              <div className="mb-8">
                <h3 className="text-3xl font-black text-neutral-900 mb-3 tracking-tight">{task.title}</h3>
                <p className="text-neutral-600 text-lg leading-relaxed font-medium">{task.description}</p>
              </div>
              <div className="p-5 bg-neutral-50 rounded-xl border border-neutral-200 mb-2">
                <div className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest mb-3">
                  Activity Log Entry
                </div>
                <div className="font-mono text-sm text-neutral-800 leading-relaxed">
                  {task.history[selectedVersion]}
                </div>
              </div>

              {task.assigneeId && selectedVersion === task.history.length - 1 && (
                <div className="mt-6 flex items-center gap-4 p-5 border-[2px] border-indigo-50 md:max-w-md bg-white rounded-xl shadow-sm">
                  <div className="w-12 h-12 rounded-full bg-indigo-50 text-indigo-700 border-2 border-indigo-200 flex items-center justify-center text-lg font-bold shadow-inner">
                    {station.agents.find((a) => a.id === task.assigneeId)?.name.charAt(0) || <User size={20} />}
                  </div>
                  <div>
                    <div className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest mb-0.5">Currently Assigned To</div>
                    <div className="text-base font-bold text-neutral-900 leading-tight">
                      {station.agents.find((a) => a.id === task.assigneeId)?.name || "Unknown Agent"}
                      {(() => {
                        const a = station.agents.find((x) => x.id === task.assigneeId);
                        return a && a.role && a.role !== a.name ? (
                          <span className="ml-2 text-xs font-medium text-neutral-400">({a.role})</span>
                        ) : null;
                      })()}
                    </div>
                  </div>
                </div>
              )}
            </div>
          </div>
        </motion.div>
      )}
    </div>
  );
};

// Compute per-agent validation track record across the entire task history.
// Walks every task's `events` and `agentRuns`, finds anything that happened
// at this station, and tallies submissions vs. rejections per agent.
const computeStationHistory = (station, allTasks) => {
  const byAgent = new Map();
  station.agents.forEach((a) => {
    byAgent.set(a.id, {
      agent: a,
      runs: 0,
      submitted: 0,
      rejected: 0,
      approved: 0,
      rejections: [], // [{ taskId, taskTitle, when, feedback, failedCriteria: [...] }]
    });
  });

  for (const task of allTasks) {
    const runs = (task.agentRuns || []).filter((r) => r.stationId === station.id);
    for (const run of runs) {
      // Match by name since some sample data uses ad-hoc agent objects.
      const slot = [...byAgent.values()].find((s) => s.agent.id === run.agent?.id || s.agent.name === run.agent?.name);
      if (!slot) continue;
      slot.runs += 1;
      if (run.status === "approved") slot.approved += 1;
      if (run.status === "submitted") slot.submitted += 1;
      if (run.status === "rejected") slot.rejected += 1;
    }

    // Pull rejection details from the events log so we can show feedback.
    const stationTitle = station.title;
    const rejectionEvents = (task.events || []).filter((e) => e.phase === "rejected" && e.station === stationTitle);
    for (const ev of rejectionEvents) {
      // Find which agent owned the run that got rejected (the run on this
      // station + this iteration whose status is "rejected").
      const owningRun = (task.agentRuns || []).find((r) => r.stationId === station.id && r.iteration === ev.iteration && r.status === "rejected");
      if (!owningRun) continue;
      const slot = [...byAgent.values()].find((s) => s.agent.id === owningRun.agent?.id || s.agent.name === owningRun.agent?.name);
      if (!slot) continue;
      const failedCriteria = (ev.snapshot?.acceptance || []).filter((a) => !a.done).map((a) => a.text);
      slot.rejections.push({
        taskId: task.id,
        taskTitle: task.title,
        when: ev.when,
        iteration: ev.iteration,
        feedback: ev.feedback,
        failedCriteria,
      });
    }
  }

  return [...byAgent.values()];
};

const Stat = ({ label, value, tone = "neutral" }) => {
  const toneCls = {
    neutral: "text-neutral-900",
    indigo: "text-indigo-700",
    emerald: "text-emerald-700",
    red: "text-red-700",
    amber: "text-amber-700",
  }[tone] || "text-neutral-900";
  return (
    <div className="px-4 py-3 rounded-xl bg-neutral-50 border border-neutral-200">
      <div className="text-[10px] font-bold uppercase tracking-[0.18em] text-neutral-500 mb-1">{label}</div>
      <div className={`text-2xl font-black tracking-tight ${toneCls}`}>{value}</div>
    </div>
  );
};

const InstructionsBlock = ({ text }) => (
  <div className="bg-neutral-50 border-2 border-dashed border-neutral-200 rounded-xl p-4">
    <div className="text-[10px] font-bold uppercase tracking-[0.18em] text-neutral-500 mb-1.5">System instructions</div>
    <p className="text-[14px] leading-relaxed text-neutral-800 italic">“{text}”</p>
  </div>
);

const RejectionItem = ({ rej, onOpen }) => (
  <li className="border-2 border-red-200 bg-red-50/40 rounded-xl p-4">
    <div className="flex items-center justify-between gap-3 mb-2 flex-wrap">
      <div className="flex items-center gap-2 flex-wrap">
        <span className="text-[10px] font-mono font-semibold text-neutral-600 bg-white border border-neutral-200 px-1.5 py-0.5 rounded">{rej.taskId}</span>
        <button
          type="button"
          onClick={() => onOpen?.({ id: rej.taskId })}
          className="text-sm font-bold text-neutral-900 hover:text-red-700 underline-offset-2 hover:underline text-left"
        >
          {rej.taskTitle}
        </button>
        {rej.iteration > 1 && (
          <span className="text-[10px] font-bold uppercase tracking-widest text-red-700 bg-white border border-red-200 px-1.5 py-0.5 rounded">Iter {rej.iteration}</span>
        )}
      </div>
      <span className="text-[11px] font-medium text-neutral-500">{rej.when}</span>
    </div>
    {rej.feedback && (
      <p className="text-[13px] text-red-900 leading-snug mb-2">“{rej.feedback}”</p>
    )}
    {rej.failedCriteria && rej.failedCriteria.length > 0 && (
      <div>
        <div className="text-[10px] font-bold uppercase tracking-[0.18em] text-red-700 mb-1">Unmet criteria at time of rejection</div>
        <ul className="flex flex-col gap-1">
          {rej.failedCriteria.map((c, i) => (
            <li key={i} className="flex items-center gap-2 text-[13px] text-neutral-800">
              <span className="shrink-0 w-3.5 h-3.5 rounded-full bg-white border-2 border-red-300" />
              {c}
            </li>
          ))}
        </ul>
      </div>
    )}
  </li>
);

const AgentDetailCard = ({ slot, onOpenTask }) => {
  const { agent, runs, rejected, approved, rejections } = slot;
  const rate = runs > 0 ? Math.round((rejected / runs) * 100) : 0;
  return (
    <section className="bg-white border-2 border-neutral-200 rounded-2xl overflow-hidden">
      <header className="px-6 py-5 border-b border-neutral-100 bg-gradient-to-b from-white to-neutral-50/60 flex items-start justify-between gap-4 flex-wrap">
        <div className="flex items-center gap-3">
          <div className="w-12 h-12 rounded-full bg-indigo-500 text-white text-base font-bold flex items-center justify-center ring-2 ring-white shadow-sm">
            {agent.name.charAt(0)}
          </div>
          <div>
            <h3 className="text-xl font-black tracking-tight text-neutral-900 leading-tight">{agent.name}</h3>
            {agent.role && agent.role !== agent.name && (
              <div className="text-[11px] font-bold uppercase tracking-[0.18em] text-neutral-500 mt-0.5">{agent.role}</div>
            )}
          </div>
        </div>
        <div className="grid grid-cols-3 gap-3 min-w-[280px]">
          <Stat label="Runs" value={runs} tone="indigo" />
          <Stat label="Approved" value={approved} tone="emerald" />
          <Stat label="Rejected" value={rejected} tone={rejected > 0 ? "red" : "neutral"} />
        </div>
      </header>
      <div className="p-6 flex flex-col gap-5">
        <InstructionsBlock text={agent.instructions || "(no system instructions on file)"} />

        <div>
          <div className="flex items-center justify-between mb-3 flex-wrap gap-2">
            <div>
              <div className="text-[11px] font-bold uppercase tracking-[0.22em] text-neutral-500">Validator track record</div>
              <p className="text-[12px] text-neutral-500 mt-0.5">Times this agent’s work was caught by the validator.</p>
            </div>
            <div className={`text-[11px] font-bold uppercase tracking-widest px-2.5 py-1 rounded-full border ${
              rate === 0 ? "bg-emerald-50 text-emerald-700 border-emerald-200" :
              rate < 30 ? "bg-amber-50 text-amber-700 border-amber-200" :
              "bg-red-50 text-red-700 border-red-200"
            }`}>
              {rate}% rejection rate
            </div>
          </div>

          {rejections.length === 0 ? (
            <div className="text-[13px] text-emerald-700 font-medium bg-emerald-50/60 border-2 border-emerald-200 rounded-xl p-4 flex items-center gap-2">
              <CheckCircle size={16} /> No rejections on record.
            </div>
          ) : (
            <ul className="flex flex-col gap-3">
              {rejections.map((rej, idx) => (
                <RejectionItem key={idx} rej={rej} onOpen={onOpenTask} />
              ))}
            </ul>
          )}
        </div>
      </div>
    </section>
  );
};

const ExpandedStationView = ({ station, stations, allTasks, onClose, onOpenTask }) => {
  const slots = computeStationHistory(station, allTasks);
  const tasksHere = allTasks.filter((t) => t.area === station.id);
  const StationFlowCmp = window.StationFlow;
  const stationIdx = (stations ?? []).findIndex((s) => s.id === station.id);
  const nextStation = stationIdx >= 0 ? stations[stationIdx + 1] : null;

  return (
  <motion.div
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    className="flex-1 w-full flex flex-col bg-neutral-50 overflow-hidden relative z-40"
  >
    <div className="bg-white border-b border-neutral-200 px-8 py-5 flex items-center justify-between z-10 shrink-0 shadow-sm">
      <div className="flex items-center gap-6">
        <button onClick={onClose} className="p-2.5 bg-neutral-100 hover:bg-neutral-200 rounded-full transition-colors text-neutral-600">
          <ArrowLeft size={20} className="-ml-0.5" />
        </button>
        <div className="flex items-center gap-4">
          <div className="w-10 h-10 rounded-xl flex items-center justify-center bg-indigo-50 text-indigo-700 border-indigo-200 border-2">
            <Layers size={20} className="text-indigo-500" />
          </div>
          <div>
            <h2 className="text-2xl font-black text-neutral-800 tracking-tight leading-none mb-1">
              {station.title}
            </h2>
            <p className="text-sm font-bold uppercase tracking-widest text-neutral-400">Station detail</p>
          </div>
        </div>
      </div>
      <div className="flex gap-3">
        <div className="flex items-center gap-2 border bg-neutral-50 px-3 py-1.5 rounded-lg">
          <span className="text-xs font-bold text-neutral-400 uppercase tracking-widest">Active</span>
          <span className="text-sm font-black text-neutral-700">{tasksHere.length}</span>
        </div>
        <div className="flex items-center gap-2 border bg-neutral-50 px-3 py-1.5 rounded-lg">
          <span className="text-xs font-bold text-neutral-400 uppercase tracking-widest">Agents</span>
          <span className="text-sm font-black text-neutral-700">{station.agents.length}</span>
        </div>
      </div>
    </div>

    <div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
      <div className="max-w-5xl mx-auto flex flex-col gap-8">

        {/* Station Flow — pipeline plan for this station */}
        {StationFlowCmp && (
          <StationFlowCmp
            task={null}
            station={station}
            onToggleValidator={null}
            nextStationTitle={nextStation?.title}
          />
        )}

        {/* Validator card */}
        <section className="bg-white border-2 border-amber-200 rounded-2xl overflow-hidden">
          <header className="px-6 py-5 border-b border-amber-100 bg-gradient-to-b from-amber-50/60 to-white flex items-start gap-4">
            {(() => { const v = window.Bridge?.primaryValidator(station); return (<>
            <div className="w-12 h-12 rounded-full bg-amber-500 text-white text-base font-bold flex items-center justify-center ring-2 ring-white shadow-sm">
              {v?.name?.charAt(0)}
            </div>
            <div className="flex-1">
              <div className="text-[10px] font-bold uppercase tracking-[0.22em] text-amber-700 mb-1">Validator</div>
              <h3 className="text-xl font-black tracking-tight text-neutral-900 leading-tight">{v?.name}</h3>
              {v?.role && v.role !== v.name && (
                <div className="text-[11px] font-bold uppercase tracking-[0.18em] text-neutral-500 mt-0.5">{v.role}</div>
              )}
            </div>
            </>); })()}
          </header>
          <div className="p-6">
            <InstructionsBlock text={window.Bridge?.primaryValidator(station)?.instructions || "(no validation criteria on file)"} />
          </div>
        </section>

        {/* Agents */}
        <div>
          <div className="text-[11px] font-bold uppercase tracking-[0.22em] text-neutral-500 mb-3">Agents on this station ({station.agents.length})</div>
          <div className="flex flex-col gap-6">
            {slots.map((slot) => (
              <AgentDetailCard key={slot.agent.id} slot={slot} onOpenTask={onOpenTask} />
            ))}
          </div>
        </div>

        {/* Currently in this lane */}
        <div>
          <div className="text-[11px] font-bold uppercase tracking-[0.22em] text-neutral-500 mb-3">Currently in this lane ({tasksHere.length})</div>
          {tasksHere.length === 0 ? (
            <div className="text-center py-12 bg-white border-2 border-dashed border-neutral-200 rounded-2xl">
              <div className="w-12 h-12 bg-neutral-100 rounded-full flex items-center justify-center mx-auto mb-3 text-neutral-300">
                <Layers size={22} />
              </div>
              <div className="text-neutral-500 font-medium text-sm">No tasks currently in this lane.</div>
            </div>
          ) : (
            <ul className="flex flex-col gap-2">
              {tasksHere.map((task) => (
                <li key={task.id}>
                  <button
                    type="button"
                    onClick={() => onOpenTask?.(task)}
                    className="w-full text-left bg-white border-2 border-neutral-200 hover:border-neutral-400 rounded-xl px-4 py-3 flex items-center gap-3 transition-colors"
                  >
                    <span className="text-[10px] font-mono font-semibold text-neutral-600 bg-neutral-100 border border-neutral-200 px-1.5 py-0.5 rounded">{task.id}</span>
                    <span className="font-bold text-neutral-900 flex-1 truncate">{task.title}</span>
                    <span className={`text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-full border ${
                      task.status === "working" ? "bg-indigo-50 text-indigo-700 border-indigo-200" :
                      task.status === "validating" ? "bg-amber-50 text-amber-700 border-amber-200" :
                      task.status === "rejected" ? "bg-red-50 text-red-700 border-red-200" :
                      "bg-neutral-50 text-neutral-600 border-neutral-200"
                    }`}>{task.status}</span>
                    <ChevronRight size={16} className="text-neutral-400" />
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </div>
  </motion.div>
  );
};

// The planning / architect / orchestrator console — the honest-claude prototype runner
// (the "Run architect" panel, the Runs drawer button, and the WorkflowHeader) — is hidden
// from the operator app for now: that whole surface is being redesigned. The code stays put
// so we can iterate on it. To bring it back while working on it, set
// `window.AL_SHOW_ARCHITECT = true` before this script loads, then reload.
const SHOW_ARCHITECT_CONSOLE = typeof window !== "undefined" && window.AL_SHOW_ARCHITECT === true;

const AssemblyLineApp = () => {
  const [tasks, setTasks] = useStateLive([]);
  const [expandedStationId, setExpandedStationId] = useStateLive(null);
  const [selectedTaskId, setSelectedTaskId] = useStateLive(null);
  const [selectedTab, setSelectedTab] = useStateLive("details");
  // Agent detail modal: { agent, station, task } — opened by clicking any agent pill.
  const [selectedAgent, setSelectedAgent] = useStateLive(null);
  const openAgent = (agent, ctx = {}) => { if (agent) setSelectedAgent({ agent, station: ctx.station || null, task: ctx.task || null }); };

  // Deep link: ?task=<id>[&tab=details|history|humanReview] opens that task's
  // modal on load — used by the Teams human-review notification links. The
  // modal renders once the task appears in the loaded snapshot.
  useEffectLive(() => {
    try {
      const p = new URLSearchParams(window.location.search);
      const id = p.get("task");
      if (id) {
        setSelectedTaskId(id);
        const tab = p.get("tab");
        if (tab) setSelectedTab(tab);
      }
    } catch (e) { /* no-op */ }
  }, []);

  // Workflow runtime (new in Task 11):
  const [library, setLibrary] = useStateLive(null);              // null until loaded
  const [libraryError, setLibraryError] = useStateLive(null);
  const [workflow, setWorkflow] = useStateLive(null);            // confirmed workflow
  // Raw run record (?run= mode) — fed to RunToCanvas helpers so the canvas can
  // render the workflow's real stations + tasks instead of window.STATIONS.
  const [currentRunRecord, setCurrentRunRecord] = useStateLive(null);
  const [proposedWorkflow, setProposedWorkflow] = useStateLive(null);
  const [runs, setRuns] = useStateLive([]);
  const [backendUnreachable, setBackendUnreachable] = useStateLive(false);
  const [snapshot, setSnapshot] = useStateLive(null);        // viewer-mode data
  const [snapshotMode, setSnapshotMode] = useStateLive(null); // null=probing, true/false=decided
  // Snapshot-mode write-back (Teamwork actions): per-task in-flight ids and
  // the last action failure, surfaced as a dismissible banner.
  const [pendingActionIds, setPendingActionIds] = useStateLive([]);
  const [actionError, setActionError] = useStateLive(null);

  const [runsDrawerOpen, setRunsDrawerOpen] = useStateLive(false);

  const loadRunAsProposed = (run) => {
    if (!run?.result?.parsed || !library) return;
    const stationsArr = window.Bridge.composeStations(run.result.parsed, library);
    const workflowMeta = window.Bridge.composeWorkflow(run.result.parsed);
    setProposedWorkflow({ stations: stationsArr, workflowMeta, sourceRunId: run.id });
  };

  // Architect-run UI state (running phase).
  const [runState, setRunState] = useStateLive("idle");      // "idle" | "running"
  const [runEvents, setRunEvents] = useStateLive([]);
  const [runStartedAt, setRunStartedAt] = useStateLive(null);
  const [runElapsedSec, setRunElapsedSec] = useStateLive(0);
  const [confirmedDetailsOpen, setConfirmedDetailsOpen] = useStateLive(false);

  // Workflow-run UI state (station chain running phase).
  const [workflowRunState, setWorkflowRunState] = useStateLive("idle"); // "idle"|"running"|"done"
  const [workflowRunCurrentRole, setWorkflowRunCurrentRole] = useStateLive(null);
  const [workflowRunDecision, setWorkflowRunDecision] = useStateLive(null);
  const [activeStory, setActiveStory] = useStateLive("");

  // humanPicker: { [taskId]: picked variantId } — tracks picks for display
  // (actual state lives on the task itself via setTasks)

  // Tick elapsed time while running.
  useEffectLive(() => {
    if (runState !== "running" || !runStartedAt) return;
    const tick = () => setRunElapsedSec(Math.floor((Date.now() - runStartedAt) / 1000));
    tick();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, [runState, runStartedAt]);

  const refreshRuns = async () => {
    try {
      const list = await window.Api.listRuns();
      setRuns(list);
      setBackendUnreachable(false);
    } catch {
      setBackendUnreachable(true);
    }
  };

  const startArchitect = async (story) => {
    setActiveStory(story);
    setRunState("running");
    setRunEvents([]);
    setRunStartedAt(Date.now());
    setRunElapsedSec(0);
    let finalResult = null;
    try {
      finalResult = await window.Api.runArchitect(
        { user_message: story },
        (event, data) => {
          setRunEvents((cur) => [...cur, { event, data }]);
        },
      );
    } catch (err) {
      setBackendUnreachable(true);
      setRunState("idle");
      return;
    }
    setRunState("idle");
    setBackendUnreachable(false);

    // Compose the proposed workflow from the final result. Save story so
    // the workflow run can pass it to the task-writer.
    if (finalResult?.parsed && library) {
      const stationsArr = window.Bridge.composeStations(finalResult.parsed, library);
      const workflowMeta = window.Bridge.composeWorkflow(finalResult.parsed);
      setProposedWorkflow({ stations: stationsArr, workflowMeta, sourceRunId: finalResult.id, story });
    }
    await refreshRuns();
  };

  const confirmProposed = () => {
    if (!proposedWorkflow) return;
    setWorkflow(proposedWorkflow);
    setProposedWorkflow(null);
    setConfirmedDetailsOpen(false);
  };

  const discardProposed = () => {
    setProposedWorkflow(null);
  };

  // Inject a task card into the conveyor after a workflow run completes.
  // This is what makes validator findings visible in the conveyor UI.
  const injectWorkflowTask = (wfDone) => {
    const { station_id, role_results, decision } = wfDone;
    const station = (workflow?.stations ?? []).find((s) => s.id === station_id);
    if (!station) return;

    const taskId = `wf-${Date.now()}`;
    const now = new Date().toLocaleTimeString();
    const evts = [
      { id: `${taskId}-e1`, phase: "created", iteration: 1, when: now, message: "Task created by workflow runner." },
      { id: `${taskId}-e2`, phase: "assigned", iteration: 1, station: station.title, actor: { name: station.agents[0]?.name ?? "Worker", role: "Worker" }, when: now, message: "Worker completed implementation in sandbox repo." },
    ];

    const iq = role_results?.['validator-internal-quality'];
    if (iq) evts.push({
      id: `${taskId}-e3`,
      phase: iq.decision === 'accept' ? "approved" : "rejected",
      iteration: 1, station: station.title,
      actor: { name: "Internal Quality", role: "Validator" }, when: now,
      message: iq.summary || `IQ: ${iq.decision}`,
      feedback: iq.decision !== 'accept' ? (iq.findings || []).filter((f) => f.status === 'fail').map((f) => f.note).join('; ') : null,
      snapshot: { acceptance: (iq.findings || []).map((f) => ({ id: f.criterion, text: f.criterion, done: f.status === 'pass' })) },
    });

    const fit = role_results?.['validator-fit-to-request'];
    if (fit) evts.push({
      id: `${taskId}-e4`,
      phase: fit.decision === 'accept' ? "approved" : "rejected",
      iteration: 1, station: station.title,
      actor: { name: "Fit to Request", role: "Validator" }, when: now,
      message: fit.summary || `Fit: ${fit.decision}`,
      feedback: fit.decision !== 'accept' ? (fit.findings || []).filter((f) => f.status === 'fail').map((f) => f.note).join('; ') : null,
      snapshot: { acceptance: (fit.findings || []).map((f) => ({ id: f.criterion, text: f.criterion, done: f.status === 'pass' })) },
    });

    const orch = role_results?.orchestrator;
    if (orch) evts.push({
      id: `${taskId}-e5`,
      phase: decision === 'accept' ? "shipped" : "rejected",
      iteration: 1, station: station.title,
      actor: { name: "Orchestrator", role: "Orchestrator" }, when: now,
      message: `Orchestrator: ${decision} — ${orch.reasoning || ''}`,
    });

    const storyLabel = (workflow?.story || activeStory || "").slice(0, 60);
    setTasks((prev) => [
      ...prev,
      {
        id: taskId,
        title: storyLabel ? `"${storyLabel}${storyLabel.length < 60 ? '"' : '…"'}` : "Workflow run",
        description: "Generated by POST /workflow/run.",
        area: decision === 'accept' ? 'done' : station_id,
        status: decision === 'accept' ? 'queue' : 'rejected',
        assigneeId: station.agents[0]?.id ?? null,
        iteration: 1,
        events: evts,
        agentRuns: [{ stationId: station_id, agent: station.agents[0], status: decision === 'accept' ? 'approved' : 'rejected', iteration: 1 }],
        paused: true,
      },
    ]);
  };

  // Inject a task card with humanPicker populated after a design workflow run.
  const injectDesignWorkflowTask = (designDone) => {
    const { station_id, variants } = designDone;
    const station = (workflow?.stations ?? []).find((s) => s.id === station_id);
    if (!station) return;

    const taskId = `wf-design-${Date.now()}`;
    const now = new Date().toLocaleTimeString();
    const storyLabel = (workflow?.story || activeStory || "").slice(0, 60);

    setTasks((prev) => [
      ...prev,
      {
        id: taskId,
        title: storyLabel ? `"${storyLabel}${storyLabel.length < 60 ? '"' : '…"'}` : "Design workflow",
        description: `${variants.length} design variant${variants.length === 1 ? '' : 's'} ready for picker review.`,
        area: station_id,
        status: 'human-review',
        assigneeId: null,
        iteration: 1,
        events: [
          { id: `${taskId}-e1`, phase: 'created', iteration: 1, when: now, message: `Design workflow completed. ${variants.length} variant(s) ready for picker review.` },
        ],
        humanPicker: {
          status: 'pending',
          variants: variants.map((v) => ({
            id: v.id,
            agentId: v.agentId,
            agentName: v.agentName,
            output: v.worker_output,
            iqDecision: v.iq_decision,
            fitDecision: v.fit_decision,
          })),
          pickedVariantId: null,
        },
        paused: true,
      },
    ]);
  };

  // When the human picks a variant, update the task and move it forward.
  const pickVariant = (taskId, variantId) => {
    setTasks((cur) => cur.map((t) => {
      if (t.id !== taskId || !t.humanPicker) return t;
      const idx = stations.findIndex((s) => s.id === t.area);
      const next = stations[idx + 1];
      const now = new Date().toLocaleTimeString();
      const pickedVariant = (t.humanPicker.variants || []).find((v) => v.id === variantId);
      const stanceNote = pickedVariant?.output?.variant_stance ? `: "${pickedVariant.output.variant_stance}"` : '';
      return {
        ...t,
        humanPicker: { ...t.humanPicker, status: 'picked', pickedVariantId: variantId },
        area: next ? next.id : 'done',
        status: 'queue',
        assigneeId: null,
        events: [
          ...(t.events || []),
          {
            id: `${t.id}-pick`,
            phase: 'approved',
            iteration: t.iteration || 1,
            when: now,
            actor: { name: 'You', role: 'Human Picker' },
            station: t.area,
            message: `Picked Variant ${variantId.toUpperCase()}${stanceNote}. Moving to ${next?.title ?? 'Shipped'}.`,
          },
        ],
      };
    }));
  };

  // Route a shipped task back into the workflow based on human terminal feedback.
  // Three routes: wrong-implementation → implementation station, wrong-design →
  // design station, scope-changed → clears workflow so architect re-runs.
  const terminalRoute = (task, routeType) => {
    const now = new Date().toLocaleTimeString();
    const ROUTE_LABELS = {
      'wrong-implementation': 'Wrong implementation',
      'wrong-design':         'Wrong design',
      'scope-changed':        'Scope changed',
    };
    const routeLabel = ROUTE_LABELS[routeType] ?? routeType;

    if (routeType === 'scope-changed') {
      setTasks((cur) => cur.map((t) => {
        if (t.id !== task.id) return t;
        return {
          ...t,
          status: 'queue',
          area: 'architect-review',
          assigneeId: null,
          events: [
            ...(t.events || []),
            {
              id: `${t.id}-route-${Date.now()}`,
              phase: 'routed-back',
              iteration: t.iteration || 1,
              when: now,
              actor: { name: 'You', role: 'Human' },
              message: `${routeLabel} — sent back to architect for replanning.`,
            },
          ],
        };
      }));
      setWorkflow(null);
      return;
    }

    const targetStationId = routeType === 'wrong-implementation' ? 'frontend-implementation' : 'frontend-design';
    const targetStation = (workflow?.stations ?? []).find((s) => s.id === targetStationId)
      ?? workflow?.stations?.[0];
    if (!targetStation) return;

    setTasks((cur) => cur.map((t) => {
      if (t.id !== task.id) return t;
      return {
        ...t,
        area: targetStation.id,
        status: 'queue',
        assigneeId: null,
        paused: true,
        events: [
          ...(t.events || []),
          {
            id: `${t.id}-route-${Date.now()}`,
            phase: 'routed-back',
            iteration: t.iteration || 1,
            when: now,
            actor: { name: 'You', role: 'Human' },
            station: targetStation.title,
            message: `${routeLabel} — routed back to ${targetStation.title} for revision.`,
          },
        ],
      };
    }));
  };

  const startDesignWorkflowRun = async (station, story) => {
    setWorkflowRunState("running");
    setWorkflowRunCurrentRole(null);
    setWorkflowRunDecision(null);

    let finalResult = null;
    try {
      finalResult = await window.Api.runDesignWorkflow(
        { story, station },
        (event, data) => {
          if (event === 'role-start') setWorkflowRunCurrentRole(data?.role ?? null);
          if (event === 'parallel-workers-start') setWorkflowRunCurrentRole('workers (parallel)');
          if (event === 'parallel-workers-done') setWorkflowRunCurrentRole('validating…');
          if (event === 'design-done') finalResult = data;
        },
      );
    } catch (err) {
      setBackendUnreachable(true);
      setWorkflowRunState("idle");
      return;
    }

    setWorkflowRunState("done");
    setBackendUnreachable(false);
    if (finalResult) {
      const variantCount = finalResult.variants?.length ?? 0;
      setWorkflowRunDecision(`${variantCount} variant${variantCount === 1 ? '' : 's'} ready`);
      injectDesignWorkflowTask(finalResult);
    }
    await refreshRuns();
  };

  const startWorkflowRun = async () => {
    if (!workflow?.stations?.length) return;
    const station = workflow.stations[0];
    const story = workflow.story || activeStory || "";

    // Dispatch to the design-variant flow when the first station is frontend-design.
    if (station.id === 'frontend-design') {
      await startDesignWorkflowRun(station, story);
      return;
    }

    setWorkflowRunState("running");
    setWorkflowRunCurrentRole(null);
    setWorkflowRunDecision(null);

    let finalResult = null;
    try {
      finalResult = await window.Api.runWorkflow(
        { story, station },
        (event, data) => {
          if (event === 'role-start') setWorkflowRunCurrentRole(data?.role ?? null);
          if (event === 'workflow-done') finalResult = data;
        },
      );
    } catch (err) {
      setBackendUnreachable(true);
      setWorkflowRunState("idle");
      return;
    }

    setWorkflowRunState("done");
    setBackendUnreachable(false);
    if (finalResult) {
      setWorkflowRunDecision(finalResult.decision ?? null);
      injectWorkflowTask(finalResult);
    }
    await refreshRuns();
  };

  // hydrateFromRecord — populate UI state from a persisted run record.
  // Called both by the on-mount ?run= handler and the polling loop so both
  // paths share identical hydration logic.
  //
  // The persisted record's stations[] are the architect's raw plan objects
  // (id, position, rationale, output_contract, definition_of_done) — NOT
  // library-composed station objects. We use them directly as the workflow
  // stations since Bridge.composeStations needs the library, and the library
  // may not be loaded when we're in ?run= mode.
  //
  // awaitingApproval: if the record has record.awaitingApproval, we tag the
  // matching station object with { awaitingApproval, stationId, runId } so
  // the StationArea approval block renders.
  const hydrateFromRecord = (record) => {
    if (!record) return;
    // Keep the raw record so canvasStations/canvasTasks (below) can re-derive.
    setCurrentRunRecord(record);

    // Build enriched stations: copy the plan stations and attach approval
    // metadata to the matching one so StationArea's approval block activates.
    const rawStations = record.stations ?? [];
    const enrichedStations = rawStations.map((s) => {
      // Minimal compatibility: ensure the station has a title field.
      const base = {
        ...s,
        title: s.title ?? s.id ?? '(unknown)',
        agents: s.agents ?? [],
      };
      if (record.awaitingApproval && record.awaitingApproval.stationId === s.id) {
        return {
          ...base,
          awaitingApproval: record.awaitingApproval,
          stationId: s.id,
          runId: record.id,
        };
      }
      return base;
    });

    setWorkflow({ stations: enrichedStations, story: record.story ?? '' });
    setActiveStory(record.story ?? '');

    // Reflect terminal state.
    const isComplete = record.events?.some((e) => e.kind === 'run_complete');
    const lastDecision = record.approvals?.at(-1)?.decision;
    const isTerminal = isComplete || lastDecision === 'stopped' || lastDecision === 'rejected';
    if (isTerminal) {
      setWorkflowRunState('done');
    } else if (record.awaitingApproval) {
      setWorkflowRunState('idle');
    } else {
      setWorkflowRunState('running');
    }
    setBackendUnreachable(false);
  };

  // Load library + run list on mount.
  // When at /demo, skip the library/runs and load the pre-seeded fixture instead.
  // When ?run=<id> is present, fetch the run record and hydrate directly.
  useEffectLive(() => {
    let cancelled = false;

    // Existing legacy boot logic — relocated verbatim, runs only when the
    // snapshot probe 404s (legacy engine server). The demo / runId / default
    // branches each kick off their async work then return from legacyBoot,
    // guarded by the shared `cancelled` flag honored in the single cleanup below.
    const legacyBoot = () => {
      const isDemo = window.location.pathname === '/demo';
      const params = new URLSearchParams(window.location.search);
      const runId = params.get('run');

      if (isDemo) {
        (async () => {
          try {
            const fixture = await fetch('/demo-fixture').then((r) => r.json());
            if (!cancelled) {
              setWorkflow({ stations: fixture.stations, story: 'Demo mode — no claude CLI required' });
              setTasks(fixture.tasks);
              setBackendUnreachable(false);
            }
          } catch {
            if (!cancelled) setBackendUnreachable(true);
          }
        })();
        return;
      }

      if (runId) {
        (async () => {
          // Load the library too: stationsFromRecord needs it for humanReview /
          // agents / validators. Without it every station is humanReview:false
          // and no approval/escalation UI ever renders.
          try {
            const lib = await window.Api.getLibrary();
            if (!cancelled) setLibrary(lib);
          } catch { /* derived stations fall back to plan-only values */ }
          try {
            const record = await fetch(`/run/${runId}`).then((r) => r.ok ? r.json() : null);
            if (!cancelled && record) hydrateFromRecord(record);
            else if (!cancelled) setBackendUnreachable(true);
          } catch {
            if (!cancelled) setBackendUnreachable(true);
          }
        })();
        return;
      }

      (async () => {
        try {
          const lib = await window.Api.getLibrary();
          if (!cancelled) { setLibrary(lib); setBackendUnreachable(false); }
        } catch (err) {
          if (!cancelled) {
            setLibraryError(String(err));
            setBackendUnreachable(true);
          }
        }
        try {
          const list = await window.Api.listRuns();
          if (!cancelled) { setRuns(list); setBackendUnreachable(false); }
        } catch {
          if (!cancelled) setBackendUnreachable(true);
        }
      })();
    };

    // Snapshot mode probe: the read-only viewer serves /snapshot.json; the
    // legacy engine 404s it. Decide once on mount.
    (async () => {
      try {
        const snap = await window.Api.getSnapshot();
        if (cancelled) return;
        setSnapshot(snap);
        setSnapshotMode(true);
        setBackendUnreachable(false);
      } catch (err) {
        if (cancelled) return;
        if (err?.status === 404) {
          setSnapshotMode(false);
          legacyBoot();
        } else {
          setBackendUnreachable(true);
        }
      }
    })();

    return () => { cancelled = true; };
  }, []);

  // Polling loop — only active when ?run=<id> is in the URL.
  // Refreshes the run record every 4 seconds until the run reaches a
  // terminal state (run_complete, rejected, stopped).
  useEffectLive(() => {
    const params = new URLSearchParams(window.location.search);
    const runId = params.get('run');
    if (!runId) return undefined;

    let cancelled = false;
    let timer = null;

    const tick = async () => {
      if (cancelled) return;
      try {
        const r = await fetch(`/run/${runId}`).then((res) => res.ok ? res.json() : null);
        if (!r || cancelled) return;
        hydrateFromRecord(r);
        const isTerminal =
          r.events?.some((e) => e.kind === 'run_complete') ||
          ['stopped', 'rejected'].includes(r.approvals?.at(-1)?.decision);
        if (!isTerminal) {
          timer = setTimeout(tick, 4000);
        }
      } catch (err) {
        if (!cancelled) timer = setTimeout(tick, 4000);
      }
    };

    tick();
    return () => { cancelled = true; if (timer) clearTimeout(timer); };
  }, []);

  // Snapshot mode polling — re-read the data dir every 5s. Cheap: the viewer
  // just re-reads files; producers update them out of band. Schedules the next
  // tick only after each fetch settles (recursive setTimeout, like the run
  // poller above) so a slow read never lets requests pile up.
  useEffectLive(() => {
    if (snapshotMode !== true) return undefined;

    let cancelled = false;
    let timer = null;

    const tick = async () => {
      if (cancelled) return;
      try {
        const snap = await window.Api.getSnapshot();
        if (cancelled) return;
        setSnapshot(snap);
        setBackendUnreachable(false);
      } catch {
        if (cancelled) return;
        setBackendUnreachable(true);
      }
      timer = setTimeout(tick, 5000);
    };

    timer = setTimeout(tick, 5000);
    return () => { cancelled = true; if (timer) clearTimeout(timer); };
  }, [snapshotMode]);

  // Broadcast read-only (snapshot) mode to sibling component trees — notably
  // Root's header "Inject Task" button, which lives outside this component.
  // A window flag covers listeners that mount/read late; the event covers
  // those already listening. Only fires once snapshotMode is decided.
  useEffectLive(() => {
    if (snapshotMode === null) return;
    window.AL_READONLY = snapshotMode === true;
    window.dispatchEvent(new CustomEvent('al-readonly-mode', { detail: { readOnly: snapshotMode === true } }));
  }, [snapshotMode]);

  const stations = workflow?.stations ?? [];

  // When ?run=<id> is active, derive the canvas's stations + tasks from the raw
  // run record so the canvas shows the workflow's real stations (not the fixed
  // window.STATIONS metaphor). Falls back to window.STATIONS / live tasks when
  // there's no run record or the helper bundle hasn't loaded yet.
  const runIdFromUrl = (typeof window !== 'undefined')
    ? new URLSearchParams(window.location.search).get('run')
    : null;
  const canvasStations = (snapshotMode && snapshot && window.DataToCanvas)
    ? window.DataToCanvas.stationsFromSnapshot(snapshot)
    : (runIdFromUrl && currentRunRecord && window.RunToCanvas)
      ? window.RunToCanvas.stationsFromRecord(currentRunRecord, library)
      : null; // null → AssemblyCanvas falls back to window.STATIONS
  const canvasTasks = (snapshotMode && snapshot && window.DataToCanvas)
    ? window.DataToCanvas.tasksFromSnapshot(snapshot).map((t) =>
        pendingActionIds.includes(t.id) ? { ...t, actionPending: true } : t)
    : (runIdFromUrl && currentRunRecord && window.RunToCanvas)
      ? window.RunToCanvas.tasksFromRecord(currentRunRecord)
      : tasks;

  // Snapshot-mode decision: POST to the viewer-server, which writes back to
  // Teamwork, re-syncs the data dir, and returns the fresh snapshot. No
  // optimistic mutation — the board only ever shows synced truth.
  const snapshotAction = async (verb, task) => {
    if (pendingActionIds.includes(task.id)) return;
    let note;
    if (verb === 'reject') {
      const reason = window.prompt(`Why reject "${task.title}"? The station lead reworks the task using this. (Blank = generic note; Cancel = leave it awaiting review.)`);
      if (reason === null) return; // cancelled — don't reject
      note = reason.trim() || undefined;
    }
    setPendingActionIds((ids) => [...ids, task.id]);
    setActionError(null);
    // Backstop: clear the pending (spinner) state even if we never observe the
    // task leaving human-review on a poll (e.g. it bounced back through a gate).
    const safety = setTimeout(() => {
      setPendingActionIds((ids) => ids.filter((id) => id !== task.id));
    }, 30000);
    try {
      const result = await window.Api.postAction(verb, task.id, note);
      if (result?.snapshot) setSnapshot(result.snapshot);
      // Deliberately do NOT clear pending here: postAction only writes the
      // decision file. The conductor reconciles + moves the card + writes back
      // to Teamwork over the next few seconds — the spinner stays until a poll
      // shows the task has actually left human-review (pruned by the effect
      // below) or the backstop above fires.
    } catch (err) {
      clearTimeout(safety);
      setPendingActionIds((ids) => ids.filter((id) => id !== task.id));
      setActionError(`Could not ${verb} "${task.title}": ${err.message}`);
    }
  };

  // Clear a task's pending (spinner) state once a poll shows it has actually
  // left human-review — i.e. the approve/reject took effect on the board.
  useEffectLive(() => {
    if (!pendingActionIds.length || !snapshot || !window.DataToCanvas) return;
    const fresh = window.DataToCanvas.tasksFromSnapshot(snapshot);
    setPendingActionIds((ids) => {
      const next = ids.filter((id) => {
        const t = fresh.find((x) => x.id === id);
        return t && t.status === 'human-review'; // still awaiting → keep spinner; moved/gone → clear
      });
      return next.length === ids.length ? ids : next;
    });
  }, [snapshot]);

  const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
    "humanReviewLayout": "detour",
    "viewMode": "canvas",
    "panMode": "scroll"
  }/*EDITMODE-END*/;
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const hrLayout = tweaks.humanReviewLayout || "detour";
  const viewMode = tweaks.viewMode || "canvas";
  const panMode = tweaks.panMode || "scroll";

  const updateTask = (taskId, updates) => {
    setTasks((current) => current.map((t) => (t.id === taskId ? (typeof updates === "function" ? { ...t, ...updates(t) } : { ...t, ...updates }) : t)));
  };

  const addTask = () => {
    const id = `t-${Math.floor(Math.random() * 1000) + 200}`;
    // Tasks come from Teamwork only — tag injected tasks accordingly so the
    // source provenance shows up immediately on the card.
    const source = { kind: "teamwork", ref: `TW-${Math.floor(Math.random() * 9000) + 1000}`, url: "https://example.teamwork.com/" };
    const newTask = {
      id,
      title: "New Feature Request",
      source,
      description: "Pending scope definition.",
      objective: "Define the scope, owners, and acceptance criteria so this can enter the line.",
      relevantFiles: [],
      subtasks: [],
      acceptance: [],
      area: "pm",
      status: "queue",
      assigneeId: null,
      iteration: 1,
      events: [
        { id: `${id}-e1`, phase: "created", iteration: 1, actor: null, when: "just now", message: "Task injected into PM intake." },
      ],
    };
    setTasks([...tasks, newTask]);
  };

  // Listen for header-driven Inject Task clicks (button now lives in main.jsx).
  useEffectLive(() => {
    const handler = () => addTask();
    window.addEventListener("assembly-line:inject-task", handler);
    return () => window.removeEventListener("assembly-line:inject-task", handler);
  }, [tasks]);

  const toggleSubtask = (taskId, subId) => {
    setTasks((cur) => cur.map((t) => t.id !== taskId ? t : {
      ...t,
      subtasks: (t.subtasks || []).map((s) => s.id === subId ? { ...s, done: !s.done } : s),
    }));
  };

  const toggleAcceptance = (taskId, acId) => {
    setTasks((cur) => cur.map((t) => t.id !== taskId ? t : {
      ...t,
      acceptance: (t.acceptance || []).map((a) => a.id === acId ? { ...a, done: !a.done } : a),
    }));
  };

  // Toggle validator on/off after a specific agent step in a task's flow.
  // If the task has no flow yet, materialize one from the station default
  // before flipping the bit.
  const toggleValidator = (taskId, stepIndex) => {
    setTasks((cur) => cur.map((t) => {
      if (t.id !== taskId) return t;
      const station = stations.find((s) => s.id === t.area);
      if (!station) return t;
      const baseSteps = t.flow?.steps && t.flow.steps.length
        ? t.flow.steps
        : (station.defaultFlow || station.agents.map((a) => a.id))
            .map((id) => ({ agentId: id, validateAfter: true }));
      const nextSteps = baseSteps.map((s, i) =>
        i === stepIndex ? { ...s, validateAfter: !(s.validateAfter !== false) } : s
      );
      return { ...t, flow: { ...(t.flow || {}), steps: nextSteps } };
    }));
  };

  const appendEvent = (task, ev) => ({
    events: [...(task.events || []), { id: `${task.id}-e${(task.events || []).length + 1}`, when: "just now", ...ev }],
  });

  const assignTask = (task, station) => {
    const randomAgent = station.agents[Math.floor(Math.random() * station.agents.length)];
    const iter = task.iteration || 1;
    updateTask(task.id, {
      status: "working",
      assigneeId: randomAgent.id,
      ...appendEvent(task, {
        phase: "assigned",
        iteration: iter,
        actor: { name: randomAgent.name, role: randomAgent.role },
        station: station.title,
        message: `${randomAgent.name} picked up the task.`,
      }),
    });
  };

  const submitForValidation = (task) => {
    const station = stations.find((s) => s.id === task.area);
    const agent = station?.agents.find((a) => a.id === task.assigneeId);
    const validator = window.Bridge.primaryValidator(station);
    updateTask(task.id, {
      status: "validating",
      ...appendEvent(task, {
        phase: "submitted",
        iteration: task.iteration || 1,
        actor: agent ? { name: agent.name, role: agent.role } : null,
        station: station?.title,
        message: `Submitted to ${validator?.name} for validation.`,
        snapshot: { subtasks: (task.subtasks || []).map((s) => ({ ...s })) },
      }),
    });
  };

  const approveTask = (task) => {
    const idx = stations.findIndex((s) => s.id === task.area);
    const here = stations[idx];
    const next = stations[idx + 1];
    const hereValidator = window.Bridge.primaryValidator(here);
    if (next) {
      updateTask(task.id, {
        area: next.id,
        status: "queue",
        assigneeId: null,
        iteration: 1, // reset iteration counter for the next station
        ...appendEvent(task, {
          phase: "approved",
          iteration: task.iteration || 1,
          actor: here ? { name: hereValidator?.name, role: hereValidator?.role } : null,
          station: here?.title,
          message: `Approved. Moved to ${next.title}.`,
          snapshot: { acceptance: (task.acceptance || []).map((a) => ({ ...a })) },
        }),
      });
    } else {
      updateTask(task.id, {
        area: "done",
        status: "queue",
        assigneeId: null,
        ...appendEvent(task, {
          phase: "shipped",
          iteration: task.iteration || 1,
          actor: here ? { name: hereValidator?.name, role: hereValidator?.role } : null,
          station: here?.title,
          message: "All stations complete. Shipped!",
        }),
      });
    }
  };

  const rejectTask = (task, station) => {
    const agentId = task.assigneeId || station.agents[Math.floor(Math.random() * station.agents.length)].id;
    const agent = station.agents.find((a) => a.id === agentId);
    const iter = (task.iteration || 1) + 1;
    const validator = window.Bridge.primaryValidator(station);
    updateTask(task.id, {
      status: "rejected",
      assigneeId: agent.id,
      iteration: iter,
      ...appendEvent(task, {
        phase: "rejected",
        iteration: task.iteration || 1,
        actor: { name: validator?.name, role: validator?.role },
        station: station.title,
        message: `Rejected — sent back to ${agent.name} for revision (iteration ${iter}).`,
        feedback: "Validator requested changes. See acceptance criteria for unmet items.",
        snapshot: { acceptance: (task.acceptance || []).map((a) => ({ ...a })) },
      }),
    });
  };

  // Pause/resume the auto-runner for a single task. When paused, the manual
  // override buttons appear on the card. When resumed, the auto-runner takes
  // over again (auto-runner itself is a future addition; for now this just
  // toggles the visibility of the manual controls).
  const togglePause = (task) => {
    setTasks((cur) => cur.map((t) => t.id === task.id ? { ...t, paused: !t.paused } : t));
  };

  const addHumanReviewComment = (taskId, text) => {
    setTasks((cur) => cur.map((t) => {
      if (t.id !== taskId) return t;
      const thread = t.humanReviewThread || [];
      return {
        ...t,
        humanReviewThread: [
          ...thread,
          {
            id: `hr-${t.id}-${thread.length + 1}`,
            author: "You",
            avatar: "Y",
            when: "just now",
            text,
          },
        ],
      };
    }));
  };

  // In snapshot mode decisions go through the viewer-server's Teamwork
  // write-back; in legacy/demo mode they mutate local simulation state.
  const onApproveHandler = snapshotMode ? (t) => snapshotAction('approve', t) : approveTask;
  const onRejectHandler = snapshotMode ? (t) => snapshotAction('reject', t) : rejectTask;

  const renderTask = (task, station) => (
    <TaskCard
      key={task.id}
      task={task}
      station={station}
      onAssign={assignTask}
      onSubmit={submitForValidation}
      onApprove={onApproveHandler}
      onReject={onRejectHandler}
      onTogglePause={togglePause}
      onTerminalRoute={terminalRoute}
      onOpenAgent={(agent, ctx) => openAgent(agent, { station: ctx?.station || station, task: ctx?.task })}
      onOpen={(t, opts) => {
        setSelectedTaskId(t.id);
        // Design tasks with pending picker open directly on the variants tab.
        const defaultTab = t.humanPicker?.status === 'pending' ? 'variants' : (opts?.tab || "details");
        setSelectedTab(defaultTab);
      }}
    />
  );

  const StationAreaCmp = window.StationArea;

  // Resolve modal/overview lookups against what the canvas actually renders:
  // in snapshot (and ?run=) mode the canvas shows canvasTasks/canvasStations,
  // while the legacy `tasks`/`stations` state is empty. In legacy default mode
  // canvasTasks === tasks and canvasStations is null → identical to before.
  const lookupTasks = canvasTasks ?? tasks;
  const lookupStations = (canvasStations && canvasStations.length) ? canvasStations : stations;

  // Live task lookup so the modal always reflects current state.
  const selectedTask = selectedTaskId ? lookupTasks.find((t) => t.id === selectedTaskId) : null;
  const selectedStation = selectedTask ? lookupStations.find((s) => s.id === selectedTask.area) : null;

  // Unknown formatVersion: refuse to render (better than rendering wrong).
  if (snapshotMode && snapshot?.versionError) {
    return (
      <div className="min-h-[60vh] flex items-center justify-center p-8">
        <div className="max-w-lg bg-red-50 border border-red-200 rounded-xl p-6 text-center">
          <h2 className="text-lg font-bold text-red-800 mb-2">Incompatible data format</h2>
          <p className="text-sm text-red-700">
            This data directory is format version <b>{snapshot.versionError.found}</b>;
            this viewer supports version <b>{snapshot.versionError.supported}</b>.
          </p>
        </div>
      </div>
    );
  }

  return (
    <div className="bg-white">
      {snapshotMode && snapshot?.meta?.lastError && (
        <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 text-sm text-amber-800">
          ⚠ Data may be stale — last sync failed at {snapshot.meta.lastError.at}: {snapshot.meta.lastError.message}
        </div>
      )}
      {snapshotMode && snapshot?.errors?.length > 0 && (
        <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 text-sm text-amber-800">
          ⚠ {snapshot.errors.length} data file{snapshot.errors.length === 1 ? '' : 's'} failed to parse and {snapshot.errors.length === 1 ? 'is' : 'are'} not shown.
        </div>
      )}
      {snapshotMode && actionError && (
        <div className="bg-red-50 border-b border-red-200 px-4 py-2 text-sm text-red-800 flex items-center justify-between gap-3">
          <span>⚠ {actionError}</span>
          <button
            onClick={() => setActionError(null)}
            className="text-xs font-bold text-red-700 border border-red-300 rounded-md px-2 py-1 hover:bg-red-100 transition shrink-0"
          >
            Dismiss
          </button>
        </div>
      )}
      {libraryError && !library && window.location.pathname !== '/demo' && (
        <div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
          <div className="max-w-md bg-white rounded-2xl shadow-xl p-6">
            <h2 className="text-lg font-black text-red-700 mb-2">Station library not loaded</h2>
            <p className="text-sm text-neutral-700 mb-3">
              The backend could not serve <code>/station-library.json</code>. The workflow runtime can't proceed without it.
            </p>
            <pre className="text-xs bg-neutral-50 border border-neutral-200 rounded p-3 overflow-auto">{libraryError}</pre>
            <p className="text-xs text-neutral-500 mt-3">
              Check that <code>honest-claude/honest-claude-assembly-line-draft/station-library.json</code> exists and that the server's <code>PROMPTS_DIR</code> resolves to that folder.
            </p>
          </div>
        </div>
      )}
      {/* demo-mode ops banner intentionally hidden — not shown to end users */}
      {new URLSearchParams(window.location.search).get('run') && (
        <div className="bg-amber-600 text-white text-center text-sm font-semibold py-2 px-4">
          Watching run <code className="bg-amber-700 px-1 rounded">{new URLSearchParams(window.location.search).get('run')}</code> — polling every 4s. Approve or reject below when a station pauses for review.
        </div>
      )}
      {backendUnreachable && (
        <div className="bg-red-600 text-white text-center text-sm font-bold py-2 px-4">
          Backend unreachable — start it with <code className="bg-red-700 px-1 rounded">npm start</code> in <code className="bg-red-700 px-1 rounded">assembly-line-frontend/</code>.
        </div>
      )}
      {snapshotMode !== true && SHOW_ARCHITECT_CONSOLE && (
        <div className="flex items-center justify-end px-8 py-2 bg-white border-b border-neutral-100">
          <button
            type="button"
            onClick={() => setRunsDrawerOpen(true)}
            className="text-xs font-bold text-neutral-600 hover:text-neutral-900 px-3 py-1.5 rounded-lg border border-neutral-300 hover:bg-neutral-50"
          >
            Runs ({runs.length})
          </button>
        </div>
      )}
      {snapshotMode !== true && SHOW_ARCHITECT_CONSOLE && (
        <WorkflowHeader
          state={
            runState === "running" ? "running"
            : proposedWorkflow ? "proposed"
            : workflow && !confirmedDetailsOpen ? "confirmed"
            : workflow && confirmedDetailsOpen ? "proposed"   // re-uses the proposed panel UI
            : "empty"
          }
          proposedWorkflow={
            confirmedDetailsOpen ? workflow : proposedWorkflow
          }
          workflow={workflow}
          runEvents={runEvents}
          runElapsedSec={runElapsedSec}
          unknownIds={window.Bridge?.unknownStations(proposedWorkflow?.stations ?? []) ?? []}
          onStartArchitect={startArchitect}
          onOpenRunsDrawer={() => setRunsDrawerOpen(true)}
          onConfirm={confirmProposed}
          onDiscard={confirmedDetailsOpen ? () => setConfirmedDetailsOpen(false) : discardProposed}
          onExpandConfirmed={() => setConfirmedDetailsOpen(true)}
          workflowRunState={workflowRunState}
          workflowRunCurrentRole={workflowRunCurrentRole}
          workflowRunDecision={workflowRunDecision}
          onRunWorkflow={startWorkflowRun}
        />
      )}
      {/* The standalone amber approval panel that used to live here was removed in
          Phase 7: approval now fires inside the canvas StationArea (Task 2.3
          placement) via each station's derived awaitingApproval field, and
          escalations render as red banners in the relevant station node. */}
      <div className="bg-neutral-50 border-y border-neutral-200" style={{ height: viewMode === "canvas" ? "auto" : 620, minHeight: viewMode === "canvas" ? 600 : undefined }}>
        {expandedStationId ? (
          <ExpandedStationView
            station={lookupStations.find((s) => s.id === expandedStationId)}
            stations={lookupStations}
            allTasks={lookupTasks}
            onClose={() => setExpandedStationId(null)}
            onOpenTask={(t) => { setSelectedTaskId(t.id); setSelectedTab("details"); }}
          />
        ) : viewMode === "canvas" && window.AssemblyCanvas ? (
          <window.AssemblyCanvas
            tasks={canvasTasks}
            stations={canvasStations}
            renderTask={renderTask}
            library={library}
            onOpenStation={(id) => setExpandedStationId(id)}
            onOpenTask={(id) => setSelectedTaskId(id)}
            onOpenAgent={openAgent}
            panMode={panMode}
            onDecision={(() => {
              const params = new URLSearchParams(window.location.search);
              const runId = params.get('run');
              if (!runId) return undefined;
              return async () => {
                try {
                  const fresh = await fetch(`/run/${runId}`).then((r) => r.ok ? r.json() : null);
                  if (fresh) hydrateFromRecord(fresh);
                } catch { /* polling will catch it */ }
              };
            })()}
          />
        ) : (
          <main className="w-full overflow-x-auto custom-scrollbar bg-neutral-50 relative">
            <div className="flex min-w-max items-stretch relative z-10 px-12 pb-12 pt-6">
              {(() => {
                const params = new URLSearchParams(window.location.search);
                const runId = params.get('run');
                const onDecisionCb = runId ? async () => {
                  try {
                    const fresh = await fetch(`/run/${runId}`).then((r) => r.ok ? r.json() : null);
                    if (fresh) hydrateFromRecord(fresh);
                  } catch { /* polling will catch it */ }
                } : undefined;
                return stations.map((s, idx) => (
                  StationAreaCmp ? (
                    <StationAreaCmp
                      key={s.id}
                      station={s}
                      tasks={tasks}
                      library={library}
                      index={idx}
                      onOpenStation={(id) => setExpandedStationId(id)}
                      onOpenTask={(id) => setSelectedTaskId(id)}
                      onOpenAgent={openAgent}
                      onDecision={onDecisionCb}
                    />
                  ) : (
                    <div key={s.id} className="text-xs text-neutral-400">Loading…</div>
                  )
                ));
              })()}

              <div className="flex flex-col shrink-0 border-l-[3px] border-neutral-200/50 pl-10 pr-16 relative min-w-[360px]">
                <div className="pt-8 pb-6 z-30 relative">
                  <h2 className="text-xl font-bold text-emerald-600 tracking-tight flex items-center gap-2">
                    <CheckCircle size={22} className="text-emerald-500" strokeWidth={2.5} />
                    Shipped Product
                  </h2>
                  <div className="text-xs text-emerald-600/70 mt-1 uppercase tracking-widest font-bold">End of Line</div>
                </div>

                <div className="flex gap-8 items-center z-10 w-full h-[360px] relative">
                  <div className="absolute top-1/2 left-0 right-0 border-t-[3px] border-dashed border-neutral-300 z-0 h-0" />
                  <div className="relative z-10 flex gap-8 items-center w-full">
                    {tasks.filter((t) => t.area === "done").map((t) => renderTask(t))}
                    {tasks.filter((t) => t.area === "done").length === 0 && (
                      <div className="w-[280px] h-32 rounded-xl flex flex-col items-center justify-center text-neutral-400/90 font-medium text-sm gap-2 bg-white border-2 border-dashed border-neutral-200 relative z-20">
                        <HardDrive size={24} />
                        No feature shipped
                      </div>
                    )}
                  </div>
                </div>
              </div>
            </div>
          </main>
        )}
      </div>

      {selectedTask && (
        <TaskModal
          task={selectedTask}
          station={selectedStation}
          onClose={() => setSelectedTaskId(null)}
          onToggleSubtask={toggleSubtask}
          onToggleAcceptance={toggleAcceptance}
          onToggleValidator={toggleValidator}
          onApprove={onApproveHandler}
          onReject={onRejectHandler}
          onAddHumanReviewComment={addHumanReviewComment}
          onPickVariant={pickVariant}
          nextStationTitle={
            selectedStation
              ? lookupStations[lookupStations.findIndex((s) => s.id === selectedStation.id) + 1]?.title || null
              : null
          }
          initialTab={selectedTab}
        />
      )}

      {selectedAgent && window.AgentModal && (
        <window.AgentModal
          agent={selectedAgent.agent}
          station={selectedAgent.station}
          task={selectedAgent.task}
          onClose={() => setSelectedAgent(null)}
        />
      )}

      {snapshotMode !== true && SHOW_ARCHITECT_CONSOLE && (
        <RunsDrawer
          open={runsDrawerOpen}
          runs={runs}
          onClose={() => setRunsDrawerOpen(false)}
          onLoadAsProposed={loadRunAsProposed}
        />
      )}

      <TweaksPanel title="Tweaks">
        <TweakSection label="View mode" />
        <TweakRadio
          label="Layout"
          value={viewMode}
          options={[
            { value: "linear", label: "Linear" },
            { value: "canvas", label: "Canvas" },
          ]}
          onChange={(v) => setTweak("viewMode", v)}
        />
        {viewMode === "canvas" && (
          <TweakRadio
            label="Pan with"
            value={panMode}
            options={[
              { value: "scroll", label: "Scroll" },
              { value: "drag", label: "Drag" },
            ]}
            onChange={(v) => setTweak("panMode", v)}
          />
        )}
        {viewMode === "linear" && (
          <>
            <TweakSection label="Human Review layout" />
            <TweakRadio
              label="Variant"
              value={hrLayout}
              options={[
                { value: "side-bay", label: "Side bay" },
                { value: "above", label: "Above belt" },
                { value: "detour", label: "Detour rail" },
              ]}
              onChange={(v) => setTweak("humanReviewLayout", v)}
            />
          </>
        )}
      </TweaksPanel>
    </div>
  );
};

window.AssemblyLineApp = AssemblyLineApp;
