UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

1,929 lines • 64.6 kB
import { ACTIVITY_LINES } from './agents';
import { changeChip } from './band';
import {
  CHANGE_SCOPE_SENTENCE,
  WHOLE_SESSION_SENTENCE,
  formatClock,
  formatStopwatch,
  formatTokens,
  formatUsd,
  roleWords,
  shortAge,
} from './cost';
import { needsAnswers, roleInitials, tableStatus } from './instruments';
import { stageRuns, stageSegments, type SegmentState } from './model';
import type { Signal } from './palette';
import { shipRail, type ShipStepState } from './ship';
import {
  FEATURE_WORDS,
  readyFeatureRoute,
  routeFromEarlierTurn,
  statusEntryRoute,
  workflowCost,
  workflowWords,
} from './status-entry';
import { NO_CHANGE } from './tabs';
import {
  seatsThatFit,
  stripLayout,
  stripWidth,
  type ChiefState,
  type DeliveryFigures,
  type DeliveryTone,
  type Mark,
  type SeatState,
  type ShipState,
  type SignoffTone,
  type StageState,
  type StripInput,
  type TableSeat,
} from './strip';
import { hasRuled, showsTable, trackRecordWords } from './table';
import { visibilityMode, type BandVisibility } from './visibility';
import { waitingOnHuman } from './attention';
import { WAITING_FOR_ANSWER, snapshotHeadline, type DesktopHeadline } from './headline';
import { IDLE_LIVE, type LiveState } from './live';
import { ELLIPSIS, fitLine } from './shell';
import {
  NOTHING_TO_PROVE,
  OLD_RECORD_NOTE,
  oldRecordGates,
  recordGapLines,
  repairLine,
  stageHasEvidence,
  trustStage,
  type TrustGateRow,
  type TrustStage,
  type WordTone,
} from './trust';
import type {
  PaqadCiJob,
  PaqadCost,
  PaqadCostLine,
  PaqadHighTable,
  PaqadHudSnapshot,
  PaqadPaneTab,
  PaqadSeat,
  PaqadSignoff,
  PaqadSignoffState,
  PaqadTimelineEntry,
  TableSitting,
} from './types';

export const READY_WORDS = 'Ready';
export const UNREADABLE = "paqad cannot read this session's record";
export const UNREADABLE_WHY = {
  'session-id': 'The session id could not be read.',
  snapshot: 'The snapshot file is not valid.',
} as const;
export const GETTING_READY = 'Paqad Getting Ready';
export const RECORD_MISSING = "paqad could not find this session's record at";
export const RECORD_MISSING_HINT = 'Run paqad-ai doctor to see why.';
export const RECORD_WAIT_MS = 30_000;
export { FEATURE_WORDS };
export const CLOSED_WORDS = 'Closed for this session';
export const HIDDEN_WORDS = 'Hidden';
export const NEEDS_YOU_WORDS = WAITING_FOR_ANSWER;
export const CLOSE_MENU_QUESTION = 'Close the paqad band?';

export const CARD_ROWS = { identity: 1, message: 1, gap: 1, strip: 3, menu: 1 } as const;

export type Unreadable = keyof typeof UNREADABLE_WHY;

const CONFLICT_CATEGORY = 'spec.expert_conflict';

export const LANE_WORDS: Readonly<Record<string, string>> = {
  fast: 'Fast lane',
  graduated: 'Graduated lane',
  full: 'Full lane',
};

const FULL_STAGES: ReadonlyArray<readonly [string, string]> = [
  ['ticket_intake', 'Intake'],
  ['planning', 'Plan'],
  ['specification', 'Spec'],
  ['development', 'Build'],
  ['review', 'Review'],
  ['checks', 'Checks'],
  ['documentation_sync', 'Docs'],
  ['delivery', 'Deliver'],
];

const OLD_ENGINE_FAST_STAGES = new Set(['planning', 'development', 'checks']);

const BOOKEND_STAGES = new Set(['ticket_intake', 'delivery']);

const WORKING_STAGES = FULL_STAGES.filter(([stage]) => !BOOKEND_STAGES.has(stage));

export function laneStages(
  snapshot: Pick<PaqadHudSnapshot, 'lane' | 'lane_stages'>,
): ReadonlyArray<readonly [string, string]> {
  const fast = snapshot.lane === 'fast';
  const required = snapshot.lane_stages;
  if (!required) {
    return fast
      ? FULL_STAGES.filter(([stage]) => OLD_ENGINE_FAST_STAGES.has(stage))
      : WORKING_STAGES;
  }
  return WORKING_STAGES.filter(([stage]) => required.includes(stage));
}

export function stageWord(stage: string): string {
  return FULL_STAGES.find(([id]) => id === stage)?.[1] ?? stage;
}

function plural(count: number, one: string, many: string): string {
  return `${count} ${count === 1 ? one : many}`;
}

function capitalised(words: string): string {
  return words.length > 0 ? `${words[0]!.toUpperCase()}${words.slice(1)}` : words;
}

function prName(snapshot: PaqadHudSnapshot): string {
  const number = snapshot.ship?.pr_number ?? null;
  return number === null ? 'The PR' : `PR #${number}`;
}

export { WAITING_FOR_ANSWER, type DesktopHeadline };

export const desktopHeadline = snapshotHeadline;

export const COST_COUNTING = 'Counting tokens';
export const COST_COUNTING_MONEY = 'The money figure follows the first counted tokens.';

function stillCounting(cost: NonNullable<PaqadHudSnapshot['cost']>): boolean {
  return cost.tokens.total === 0 && !cost.sealed;
}

export function costWords(cost: PaqadHudSnapshot['cost']): string | null {
  if (!cost) return null;
  if (stillCounting(cost)) return COST_COUNTING;
  const tokens = `${formatTokens(cost.tokens.total)} tokens`;
  if (cost.scope === 'whole-session') return `${tokens} · whole session`;
  return cost.usd === null ? tokens : `${tokens} · ~${formatUsd(cost.usd)}`;
}

export interface IdentityView {
  parts: string[];
  cost: string | null;
  flags: number;
}

export const CONTROLS_COLUMNS = 16;
const BRAND_COLUMNS = 'paqad'.length;
const ROW_GAP_COLUMNS = 1;

function identityColumns(parts: string[], cost: string | null, flags: number): number {
  const left = BRAND_COLUMNS + identityText({ parts, cost: null, flags }).length;
  const right =
    (cost ? cost.length + ROW_GAP_COLUMNS : 0) +
    (flags > 0 ? flagLabel(flags).length + ROW_GAP_COLUMNS : 0) +
    CONTROLS_COLUMNS;
  return left + ROW_GAP_COLUMNS + right;
}

export function identityView(
  snapshot: PaqadHudSnapshot,
  columns = Number.POSITIVE_INFINITY,
): IdentityView {
  const active = snapshot.active;
  const lane = active && snapshot.lane ? (LANE_WORDS[snapshot.lane] ?? null) : null;
  const chip = changeChip(snapshot);
  const flags = active ? snapshot.flags.length : 0;
  const cost = active ? costWords(snapshot.cost) : null;
  const keep = (...parts: Array<string | null>) =>
    parts.filter((part): part is string => part !== null);
  const steps: IdentityView[] = [
    { parts: keep(FEATURE_WORDS, lane, chip), cost, flags },
    { parts: keep(FEATURE_WORDS, lane, chip), cost: null, flags },
    { parts: keep(FEATURE_WORDS, chip), cost: null, flags },
  ];
  return (
    steps.find((step) => identityColumns(step.parts, step.cost, flags) <= columns) ?? {
      parts: keep(chip),
      cost: null,
      flags,
    }
  );
}

export function identityText(identity: IdentityView): string {
  return `   ${identity.parts.join('  ·  ')}`;
}

export function flagWords(flags: number): string {
  return plural(flags, 'flag', 'flags');
}

export function flagLabel(flags: number): string {
  return `⚑ ${flagWords(flags)}`;
}

function paused(snapshot: PaqadHudSnapshot, live: LiveState): boolean {
  return live.waitingOn === 'question' || snapshot.decisions.pending.length > 0;
}

const STAGE_STATE: Readonly<Record<SegmentState, StageState>> = {
  done: 'done',
  running: 'run',
  marked: 'marked',
  failed: 'fail',
  'not required': 'skip',
  'not yet': 'todo',
};

function markedValue(snapshot: PaqadHudSnapshot, stage: string): string {
  const record = snapshot.stages.find((entry) => entry.stage === stage);
  const word = stageWord(stage);
  if (record?.no_report) return `${word} · ${HELPER_NO_REPORT}`;
  if (record?.record_gap) return `${word} marked: no record`;
  if (record?.duration_unreliable) return `${word} marked: no recorded work`;
  if (record?.state === 'inconclusive') return `${word} inconclusive`;
  if (stage === 'checks') return 'Checks: no test run recorded';
  return `${word} marked`;
}

export const HELPER_NO_REPORT = 'helper gave no report';
export const CHANGE_CLOSED = 'Change closed';
export const NO_VERDICT_WORDS = 'no verdict record';
export const DELIVERY_MISSING = 'Delivery missing';
export const NEW_COMMITS_AFTER_CLOSE = 'new commits after close';
export const SIGNOFF_LABEL = 'Sign-off';

export function closedChangeBand(snapshot: PaqadHudSnapshot): DesktopBand | null {
  const closed = snapshot.closed_change;
  if (!closed) return null;
  const number = snapshot.ship?.pr_number ?? null;
  const href = snapshot.ship?.pr_url ?? null;
  return {
    kind: 'closed-change',
    word: closed.word ?? NO_VERDICT_WORDS,
    pr: number !== null && href ? { text: `PR #${number}`, href } : null,
    note: closed.new_commits > 0 ? NEW_COMMITS_AFTER_CLOSE : null,
  };
}
export const CHECK_CEILING_MS = 10 * 60_000;
export const COPIED_MS = 2_000;

export interface SignoffCheck {
  ref: string | null;
  at: number;
}

export type SignoffShown = PaqadSignoffState | 'checking';
export type SignoffAction = 'check' | 'view' | 'copy';

export interface SignoffView {
  state: SignoffShown;
  bar: StageState;
  words: string | null;
  tone: SignoffTone | null;
  action: SignoffAction | null;
  label: string | null;
  name: string | null;
  ref: string | null;
}

export interface SignoffInput {
  now: number;
  check?: SignoffCheck | null;
  design?: boolean;
  copiedAt?: number | null;
}

export function signoffCheckPrompt(ref: string | null): string {
  return `Check the design sign-off for ${ref ?? 'this change'}: read the replies with your connector and run paqad-ai hld check-approval.`;
}

export function checkPending(
  signoff: PaqadSignoff | null | undefined,
  check: SignoffCheck | null | undefined,
  now: number,
): boolean {
  if (!signoff || !check || signoff.state !== 'awaiting_sign_off') return false;
  if (check.ref !== signoff.ref) return false;
  if (!(now - check.at < CHECK_CEILING_MS)) return false;
  const checked = Date.parse(signoff.checked_at ?? '');
  return !(Number.isFinite(checked) && checked >= check.at);
}

function ageSince(at: string | null, now: number): string | null {
  const from = Date.parse(at ?? '');
  return Number.isFinite(from) ? shortAge(now - from) : null;
}

const SIGNOFF_BAR: Readonly<Record<SignoffShown, StageState>> = {
  pending: 'todo',
  writing: 'run',
  awaiting_sign_off: 'paused',
  checking: 'paused',
  approved: 'done',
  changes_requested: 'todo',
  skipped: 'skip',
  not_posted: 'skip',
};

export function signoffView(
  signoff: PaqadSignoff | null | undefined,
  input: SignoffInput,
): SignoffView | null {
  if (!signoff) return null;
  const ref = signoff.ref;
  const state: SignoffShown = checkPending(signoff, input.check, input.now)
    ? 'checking'
    : signoff.state;
  const view = (
    words: string | null,
    tone: SignoffTone | null,
    action: SignoffAction | null = null,
    label: string | null = null,
    name: string | null = null,
  ): SignoffView => ({ state, bar: SIGNOFF_BAR[state], words, tone, action, label, name, ref });
  const named = (verb: string) => `${verb} for ${ref ?? 'this change'}`;
  switch (state) {
    case 'pending':
      return view(null, null);
    case 'writing':
      return view('Writing design', 'acct');
    case 'awaiting_sign_off': {
      const age = ageSince(signoff.posted_at, input.now);
      return view(
        age ? `Waiting ${age}` : 'Waiting',
        'caut',
        'check',
        'Check',
        named('Check design sign-off'),
      );
    }
    case 'checking':
      return view('Checking…', 'acct');
    case 'approved':
      return view('Signed off', 'passt');
    case 'changes_requested':
      return view('Changes requested', 'fail', 'view', 'View', named('View design sign-off'));
    case 'skipped':
      return view('Sign-off skipped', 'mutt');
    case 'not_posted': {
      if (!input.design) return view('Not posted', 'mutt');
      const copied = typeof input.copiedAt === 'number' && input.now - input.copiedAt < COPIED_MS;
      return view(
        'Not posted',
        'mutt',
        'copy',
        copied ? 'Copied' : 'Copy design',
        named('Copy design'),
      );
    }
  }
}

export interface StageStrip {
  stages: Array<Mark<StageState>>;
  value: string;
  word: string | null;
}

export function stageStrip(
  snapshot: PaqadHudSnapshot,
  live: LiveState = IDLE_LIVE,
  signoff: SignoffView | null = null,
): StageStrip {
  const segments = stageSegments(snapshot);
  const hold = paused(snapshot, live);
  const stages = laneStages(snapshot).map(([stage, label]) => {
    const state = segments.find((segment) => segment.stage === stage)?.state ?? 'not yet';
    const mark: StageState = state === 'running' && hold ? 'paused' : STAGE_STATE[state];
    const respec = stage === 'specification' && signoff?.state === 'changes_requested';
    return { stage, label, state: respec && mark !== 'fail' ? 'run' : mark };
  });
  if (signoff) {
    const build = stages.findIndex((entry) => entry.stage === 'development');
    stages.splice(build < 0 ? stages.length : build, 0, {
      stage: 'design_sign_off',
      label: SIGNOFF_LABEL,
      state: signoff.bar,
    });
  }
  const running = stages.find((entry) => entry.state === 'run' || entry.state === 'paused');
  const failed = stages.find((entry) => entry.state === 'fail');
  const marked = stages.find((entry) => entry.state === 'marked');
  const drawn = stages.filter((entry) => entry.state !== 'skip');
  let value = '';
  if (running) {
    value =
      running.state === 'paused' && running.label !== SIGNOFF_LABEL
        ? `${running.label} · paused`
        : running.label;
  } else if (failed) value = `${failed.label} failed`;
  else if (marked) value = markedValue(snapshot, marked.stage);
  else if (snapshot.active && drawn.length > 0 && drawn.every((entry) => entry.state === 'done')) {
    value = 'All stages done';
  }
  return {
    stages: stages.map(({ label, state }) => ({ label, state })),
    value,
    word: running ? running.label : null,
  };
}

export interface TableStrip {
  seats: Array<Mark<SeatState>> | null;
  chief: ChiefState;
  value: string;
  hidden: number;
}

export const TABLE_SEATS_SHOWN = 3;

const SEAT_ORDER: Readonly<Record<SeatState, number>> = {
  writing: 0,
  late: 1,
  done: 2,
  seated: 3,
};

export const NEEDS_ANSWERS_WORDS = 'Needs answers';

export function chiefState(snapshot: PaqadHudSnapshot): ChiefState {
  const table = snapshot.table;
  if (!table?.chief) return 'waiting';
  if (table.chief.state === 'writing') return 'ruling';
  if (hasRuled(table)) return needsAnswers(snapshot, table) ? 'needs' : 'done';
  return 'waiting';
}

function doneAt(seat: PaqadSeat): number {
  const at = Date.parse(seat.done_at ?? '');
  return Number.isFinite(at) ? at : Number.NEGATIVE_INFINITY;
}

function liveFirst(seats: readonly PaqadSeat[]): Array<Mark<SeatState>> {
  return seats
    .map((seat) => ({ seat, state: (seat.late ? 'late' : seat.state) as SeatState }))
    .sort((a, b) => {
      const order = SEAT_ORDER[a.state] - SEAT_ORDER[b.state];
      if (order !== 0 || a.state !== 'done') return order;
      const newer = doneAt(b.seat) - doneAt(a.seat);
      return Number.isNaN(newer) ? 0 : newer;
    })
    .map(({ seat, state }) => ({ label: roleInitials(seat.role), state }));
}

export function tableStrip(snapshot: PaqadHudSnapshot): TableStrip {
  if (!showsTable(snapshot) || snapshot.table.skipped) {
    return { seats: null, chief: 'waiting', value: '', hidden: 0 };
  }
  const table = snapshot.table;
  const value = tableStatus(snapshot, table) === 'needs answers' ? NEEDS_ANSWERS_WORDS : '';
  const seats = liveFirst(table.seats);
  const shown = seatsThatFit(seats.length, TABLE_SEATS_SHOWN);
  return {
    seats: seats.slice(0, shown),
    chief: chiefState(snapshot),
    value,
    hidden: seats.length - shown,
  };
}

export type DeliveryLabel = 'Edit' | 'Commit' | 'Push' | 'PR' | 'CI';

const SHIP_STATE: Readonly<Record<ShipStepState, ShipState>> = {
  done: 'done',
  current: 'run',
  todo: 'todo',
  failed: 'fail',
  closed: 'fail',
  unknown: 'todo',
};

export function deliverySteps(snapshot: PaqadHudSnapshot): DeliveryLabel[] {
  const plan = snapshot.delivery_plan;
  if (!plan) return ['Edit', 'Commit', 'Push', 'PR', 'CI'];
  const steps: DeliveryLabel[] = ['Edit'];
  if (plan.commit) steps.push('Commit');
  if (plan.push) steps.push('Push');
  if (plan.pr) steps.push('PR');
  if (plan.ci) steps.push('CI');
  return steps;
}

export interface DeliveryStrip {
  steps: Array<Mark<ShipState>>;
  ci: number | null;
  value: string;
  tone: DeliveryTone;
  figures: DeliveryFigures | null;
}

export interface DeliveryLine {
  text: string;
  tone: DeliveryTone;
}

export function deliveryValue(
  snapshot: PaqadHudSnapshot,
  live: LiveState = IDLE_LIVE,
): DeliveryLine {
  const line = (text: string, tone: DeliveryTone = 'ink'): DeliveryLine => ({ text, tone });
  if (!snapshot.active) return line('', 'mut');
  const rail = shipRail(snapshot, live);
  const plan = snapshot.delivery_plan;
  const ship = snapshot.ship;
  const state = (label: string) => rail.steps.find((step) => step.label === label)?.state;
  const pr = prName(snapshot);
  if (state('PR') === 'closed') return line('PR closed', 'fail');
  if (state('PR') === 'unknown') return line('PR state unknown');
  if (state('CI') === 'failed') return line('CI failed', 'fail');
  if (plan?.ci !== false) {
    if (state('CI') === 'current') return line(`${pr} · CI running`, 'acc');
    if (state('CI') === 'done') {
      return line(rail.ci ? `${pr} · ${rail.ci.done} checks green` : `${pr} · checks green`);
    }
  }
  if (ship?.pr_state === 'open' || rail.merged) return line(pr);
  if (state('Push') === 'failed') return line('Push failed', 'fail');
  if (state('Push') === 'done') {
    return line(ship?.push_branch ? `Pushed · ${ship.push_branch}` : 'Pushed');
  }
  if (plan && !plan.commit) return line('Edit only · no commit', 'mut');
  if (plan && !plan.push) return line('No push for this change', 'mut');
  if ((ship?.commits ?? 0) > 0) return line('Not pushed');
  if (state('Edit') === 'current') return line('Editing', 'acc');
  if (state('Edit') === 'done') return line('Edited, not committed');
  return line('Not started', 'mut');
}

export function deliveryFigures(snapshot: PaqadHudSnapshot): DeliveryFigures | null {
  const ship = snapshot.active ? snapshot.ship : null;
  if (!ship) return null;
  const commits = ship.commits ?? 0;
  const figures = ship.diff
    ? {
        commits,
        additions: ship.diff.additions,
        deletions: ship.diff.deletions,
        partial: ship.diff.partial,
      }
    : ship.pr_additions !== null && ship.pr_deletions !== null
      ? { commits, additions: ship.pr_additions, deletions: ship.pr_deletions }
      : null;
  return figures && figures.additions + figures.deletions > 0 ? figures : null;
}

export function deliveryStrip(
  snapshot: PaqadHudSnapshot,
  live: LiveState = IDLE_LIVE,
): DeliveryStrip {
  const rail = shipRail(snapshot, live);
  const steps = deliverySteps(snapshot).map((label) => ({
    label,
    state: SHIP_STATE[rail.steps.find((step) => step.label === label)?.state ?? 'todo'],
  }));
  const ciRunning = steps.some((step) => step.label === 'CI' && step.state === 'run');
  const line = deliveryValue(snapshot, live);
  return {
    steps,
    ci: ciRunning && rail.ci ? rail.ci.done / rail.ci.total : null,
    value: line.text,
    tone: line.tone,
    figures: deliveryFigures(snapshot),
  };
}

export const SETTLE_WINDOW_MS = 10_000;

export function settledChange(
  snapshot: PaqadHudSnapshot | null,
): { key: string; endedAt: number | null } | null {
  if (!snapshot?.active) return null;
  const required = snapshot.stages.filter((stage) => stage.mandatory);
  const ended = (state: string) => state === 'complete' || state === 'redone';
  if (required.length === 0 || !required.every((stage) => ended(stage.state))) return null;
  if (snapshot.stages.some((stage) => stage.state === 'running')) return null;
  const ends = snapshot.stages
    .map((stage) => Date.parse(stage.ended_at ?? ''))
    .filter((at) => Number.isFinite(at));
  return {
    key: snapshot.change?.dir ?? snapshot.change?.id ?? 'change',
    endedAt: ends.length > 0 ? Math.max(...ends) : null,
  };
}

export function settleStart(endedAt: number | null, firstSeenAt: number): number | null {
  if (endedAt === null) return null;
  const after = firstSeenAt - endedAt;
  return after >= 0 && after <= SETTLE_WINDOW_MS ? firstSeenAt : null;
}

export function stripOf(
  snapshot: PaqadHudSnapshot,
  columns: number,
  live: LiveState = IDLE_LIVE,
  clockMs?: number,
  settledAtMs: number | null = null,
  signoff: SignoffView | null = null,
): StripInput {
  const stage = stageStrip(snapshot, live, signoff);
  const table = tableStrip(snapshot);
  const delivery = deliveryStrip(snapshot, live);
  const running = snapshot.stages.find((entry) => entry.state === 'running');
  const runs =
    running && stage.value === stageWord(running.stage)
      ? stageRuns(running, clockMs ?? null)
      : null;
  const short = runs ? `${stage.value} · ${runs.runs}` : null;
  return {
    columns,
    stages: stage.stages,
    stageValue: runs?.span ? `${short} · ${runs.span}` : (short ?? stage.value),
    ...(short && runs?.span ? { stageValueShort: short } : {}),
    seats: table.seats,
    hiddenSeats: table.hidden,
    chief: table.chief,
    tableValue: table.value,
    deliveryValue: delivery.value,
    deliveryTone: delivery.tone,
    deliveryFigures: delivery.figures,
    ...(signoff?.words && signoff.tone
      ? {
          signoff: {
            words: signoff.words,
            tone: signoff.tone,
            button: signoff.label,
            ref: signoff.ref,
            name: signoff.name,
          },
        }
      : {}),
    ...(clockMs === undefined ? {} : { clockMs }),
    ...(settledAtMs === null ? {} : { settledAtMs }),
  };
}

const CHIEF_WORDS: Readonly<Record<ChiefState, string>> = {
  waiting: 'chief waiting',
  ruling: 'chief ruling',
  needs: NEEDS_ANSWERS_WORDS,
  done: 'chief ruled',
};

function tableSentence(strip: StripInput): string {
  const shown = strip.seats?.length ?? 0;
  const hidden = strip.hiddenSeats ?? 0;
  const seats = `${plural(shown + hidden, 'seat', 'seats')}${hidden > 0 ? `, ${shown} shown` : ''}`;
  return `High Table: ${strip.tableValue || CHIEF_WORDS[strip.chief]}, ${seats}.`;
}

function signoffAlt(strip: StripInput): string | null {
  const signoff = strip.signoff;
  if (!signoff) return null;
  const on = signoff.ref ? ` for ${signoff.ref}` : '';
  const action = signoff.button
    ? ` ${signoff.button} button: ${signoff.name ?? `${signoff.button} available`}.`
    : '';
  return `Design sign-off${on}: ${signoff.words.replace(/…$/, '')}.${action}`;
}

function deliverySentence(strip: StripInput): string {
  const parts = [`Delivery ${(strip.deliveryValue || 'Not started').replace(/\.$/, '')}.`];
  const figures = strip.deliveryFigures;
  if (figures && figures.additions + figures.deletions > 0) {
    const words = [
      ...(figures.commits > 0 ? [plural(figures.commits, 'commit', 'commits')] : []),
      `${figures.additions.toLocaleString('en-US')} lines added`,
      `${figures.deletions.toLocaleString('en-US')} lines removed`,
    ];
    parts.push(`${words.join(', ')}.`);
    if (figures.partial) parts.push('New files were partly counted.');
  }
  return parts.join(' ');
}

export function stripAltText(text: string, strip: StripInput): string {
  const parts = [`${text.replace(/\.$/, '')}.`, `Stage ${strip.stageValue || 'not started'}.`];
  const done = strip.stages.filter((mark) => mark.state === 'done').map((mark) => mark.label);
  if (done.length > 0) parts.push(`${done.join(', ')} done.`);
  const signoff = signoffAlt(strip);
  if (signoff) parts.push(signoff);
  if (strip.seats) parts.push(tableSentence(strip));
  parts.push(deliverySentence(strip));
  return parts.join(' ');
}

function clockWords(clock: string): string {
  const [seconds = 0, minutes = 0, hours = 0] = clock.split(':').map(Number).reverse();
  const words = [`${minutes} min`, `${seconds} s`];
  return `Running for ${hours > 0 ? `${hours} h ` : ''}${words.join(' ')}.`;
}

export interface StripAlts {
  stage: string;
  table: string;
  delivery: string;
}

export function stripAlts(text: string, strip: StripInput): StripAlts {
  const stage = [`${text.replace(/\.$/, '')}.`, `Stage ${strip.stageValue || 'not started'}.`];
  const done = strip.stages.filter((mark) => mark.state === 'done').map((mark) => mark.label);
  if (done.length > 0) stage.push(`${done.join(', ')} done.`);
  const signoff = signoffAlt(strip);
  if (signoff) stage.push(signoff);
  const table = tableSentence(strip);
  const delivery = deliverySentence(strip);
  const shown = stripLayout(stripWidth(strip.columns), strip.seats !== null).ids;
  if (strip.seats && !shown.includes('table')) stage.push(table);
  if (!shown.includes('delivery')) stage.push(delivery);
  return { stage: stage.join(' '), table, delivery };
}

export function headlineAlt(head: Pick<DesktopHeadline, 'text' | 'note' | 'clock'>): string {
  const parts = [`${head.text.replace(/\.$/, '')}.`];
  if (head.note) parts.push(`${head.note.replace(/\.$/, '')}.`);
  if (head.clock) parts.push(clockWords(head.clock));
  return parts.join(' ');
}

export function cardRows(menuOpen: boolean): number {
  return (
    CARD_ROWS.identity +
    CARD_ROWS.message +
    CARD_ROWS.gap +
    CARD_ROWS.strip +
    (menuOpen ? CARD_ROWS.menu : 0)
  );
}

export interface MinimisedView {
  light: Signal;
  live: boolean;
  text: string;
  stages: StageState[];
  cost: string | null;
}

function minimisedView(
  snapshot: PaqadHudSnapshot,
  head: DesktopHeadline,
  stage: StageStrip,
): MinimisedView {
  const word = head.live ? stage.word : null;
  const text = word ? (head.clock ? `${word} · ${head.clock}` : word) : stage.value || head.text;
  return {
    light: head.signal,
    live: head.live,
    text,
    stages: stage.stages.map((mark) => mark.state),
    cost: snapshot.active ? costWords(snapshot.cost) : null,
  };
}

export interface FeatureCard {
  signoff: SignoffView | null;
  mode: 'card' | 'minimised';
  menu: boolean;
  identity: IdentityView;
  headline: DesktopHeadline;
  headlineAlt: string;
  strip: StripInput;
  alt: string;
  stripAlts: StripAlts;
  minimised: MinimisedView;
  activity: string[];
}

export type DesktopBand =
  | { kind: 'none' }
  | { kind: 'ready' }
  | { kind: 'unreadable'; reason: Unreadable; headline: string }
  | { kind: 'missing'; line: string; hint: string }
  | {
      kind: 'workflow';
      words: string;
      running: boolean;
      clock: string | null;
      cost: string;
    }
  | { kind: 'closed' | 'hidden'; needsYou: boolean }
  | {
      kind: 'closed-change';
      word: string;
      pr: { text: string; href: string | null } | null;
      note: string | null;
    }
  | ({ kind: 'feature' } & FeatureCard)
  | ({ kind: 'ready-feature' } & FeatureCard);

export interface DesktopBandInput {
  snapshot: PaqadHudSnapshot | null;
  unbound: boolean;
  unavailable: boolean;
  now: number;
  turnStartedAt: number | null;
  live?: LiveState;
  visibility: BandVisibility;
  menuOpen: boolean;
  columns: number;
  maxRows: number;
  settledAtMs?: number | null;
  signoffCheck?: SignoffCheck | null;
  design?: boolean;
  copiedAt?: number | null;
  missingRecord?: { root: string; home: string | null } | null;
}

export function homeShortened(path: string, home: string | null): string {
  if (home === null || home === '' || home === '/') return path;
  if (path === home) return '~';
  for (const separator of ['/', '\\']) {
    if (path.startsWith(`${home}${separator}`)) return `~${path.slice(home.length)}`;
  }
  return path;
}

export function middleTruncated(text: string, max: number): string {
  const chars = [...text];
  const room = Math.max(1, Math.floor(max));
  if (chars.length <= room) return text;
  const cut = Math.max(chars.lastIndexOf('/'), chars.lastIndexOf('\\'));
  const tailFrom =
    cut >= 0 && chars.length - cut < room - 1 ? cut : chars.length - Math.ceil((room - 1) / 2);
  const tail = chars.slice(tailFrom);
  const head = chars.slice(0, Math.max(0, room - 1 - tail.length));
  return `${head.join('')}${ELLIPSIS}${tail.join('')}`;
}

export function recordMissingLine(root: string, home: string | null, columns: number): string {
  const room = columns - HEADLINE_INSET_COLUMNS - [...RECORD_MISSING].length - 1;
  return `${RECORD_MISSING} ${middleTruncated(homeShortened(root, home), room)}`;
}

export function unreadableReason(input: {
  snapshot: PaqadHudSnapshot | null;
  unbound: boolean;
  unavailable: boolean;
}): Unreadable | null {
  if (input.unbound) return 'session-id';
  if (input.unavailable && input.snapshot === null) return 'snapshot';
  return null;
}

function workflowBand(
  snapshot: PaqadHudSnapshot,
  route: string,
  now: number,
  turnStartedAt: number | null,
): DesktopBand {
  const workflow = snapshot.workflow?.name === route ? snapshot.workflow : null;
  return {
    kind: 'workflow',
    words: capitalised(workflowWords(route)),
    running: turnStartedAt !== null,
    clock: turnStartedAt === null ? null : formatStopwatch(now - turnStartedAt),
    cost: workflowCost(workflow) ?? 'counting',
  };
}

const HEADLINE_INSET_COLUMNS = 2;

const READY_CARD_MIN_COLUMNS = 40;

const READY_HEADLINE: DesktopHeadline = {
  row: 0,
  signal: 'idle',
  tone: 'neutral',
  live: false,
  text: GETTING_READY,
  word: null,
  note: null,
  gap: null,
  clock: null,
  activity: [],
};

function readyIdentity(snapshot: PaqadHudSnapshot): IdentityView {
  const lane = snapshot.lane ? (LANE_WORDS[snapshot.lane] ?? null) : null;
  return { parts: lane ? [FEATURE_WORDS, lane] : [FEATURE_WORDS], cost: null, flags: 0 };
}

function readyFeatureBand(snapshot: PaqadHudSnapshot, input: DesktopBandInput): DesktopBand {
  const mode = visibilityMode(input.visibility);
  const blank = { ...snapshot, lane: null, lane_stages: undefined };
  const head = {
    ...READY_HEADLINE,
    text: fitLine(GETTING_READY, input.columns - HEADLINE_INSET_COLUMNS),
  };
  const strip = stripOf(blank, input.columns, IDLE_LIVE, input.now);
  const fits = input.maxRows >= cardRows(input.menuOpen) && input.columns >= READY_CARD_MIN_COLUMNS;
  return {
    kind: 'ready-feature',
    signoff: null,
    mode: mode === 'minimised' || !fits ? 'minimised' : 'card',
    menu: input.menuOpen,
    identity: readyIdentity(snapshot),
    headline: head,
    headlineAlt: headlineAlt(READY_HEADLINE),
    strip,
    alt: stripAltText(GETTING_READY, strip),
    stripAlts: stripAlts(GETTING_READY, strip),
    minimised: minimisedView(blank, head, stageStrip(blank, IDLE_LIVE)),
    activity: [],
  };
}

export function buildDesktopBand(input: DesktopBandInput): DesktopBand {
  const unreadable = unreadableReason(input);
  if (unreadable) return { kind: 'unreadable', reason: unreadable, headline: UNREADABLE };
  const snapshot = input.snapshot;
  if (!snapshot && input.missingRecord) {
    return {
      kind: 'missing',
      line: recordMissingLine(input.missingRecord.root, input.missingRecord.home, input.columns),
      hint: RECORD_MISSING_HINT,
    };
  }
  if (!snapshot) return { kind: 'ready' };
  const route = statusEntryRoute(snapshot);
  if (route) {
    if (routeFromEarlierTurn(snapshot, input.turnStartedAt)) return { kind: 'none' };
    return workflowBand(snapshot, route, input.now, input.turnStartedAt);
  }
  const closedChange = closedChangeBand(snapshot);
  if (!snapshot.active && !readyFeatureRoute(snapshot) && !closedChange) return { kind: 'none' };
  const live = input.live ?? IDLE_LIVE;
  const mode = visibilityMode(input.visibility);
  if (mode === 'closed' || mode === 'hidden') {
    return { kind: mode, needsYou: waitingOnHuman(snapshot, live) };
  }
  if (closedChange) return closedChange;
  if (!snapshot.active) {
    return routeFromEarlierTurn(snapshot, input.turnStartedAt)
      ? { kind: 'none' }
      : readyFeatureBand(snapshot, input);
  }
  const full = desktopHeadline(snapshot, input.now, live);
  const head = { ...full, text: fitLine(full.text, input.columns - HEADLINE_INSET_COLUMNS) };
  const signoff = signoffView(snapshot.signoff, {
    now: input.now,
    check: input.signoffCheck ?? null,
    design: input.design === true,
    copiedAt: input.copiedAt ?? null,
  });
  const strip = stripOf(
    snapshot,
    input.columns,
    live,
    input.now,
    input.settledAtMs ?? null,
    signoff,
  );
  const fits = input.maxRows >= cardRows(input.menuOpen);
  return {
    kind: 'feature',
    signoff,
    mode: mode === 'minimised' || !fits ? 'minimised' : 'card',
    menu: input.menuOpen,
    identity: identityView(snapshot, input.columns),
    headline: head,
    headlineAlt: headlineAlt(full),
    strip,
    alt: stripAltText(full.text, strip),
    stripAlts: stripAlts(full.text, strip),
    minimised: minimisedView(snapshot, head, stageStrip(snapshot, live, signoff)),
    activity: full.activity.slice(0, ACTIVITY_LINES),
  };
}

export function bandIsLive(band: DesktopBand): boolean {
  if (band.kind === 'workflow') return band.running;
  return band.kind === 'feature' && band.headline.live;
}

export type { WordTone };

export interface LightView {
  signal: Signal;
  title: string;
  why: string | null;
}

export interface LedgerTab {
  id: PaqadPaneTab;
  label: string;
  width: string;
  shown: boolean;
}

export const LEDGER_TABS: ReadonlyArray<{ id: PaqadPaneTab; label: string }> = [
  { id: 'trust', label: 'Trust' },
  { id: 'table', label: 'High Table' },
  { id: 'ships', label: 'Delivery' },
  { id: 'record', label: 'Record' },
  { id: 'cost', label: 'Cost' },
];

export function tableSat(
  snapshot: PaqadHudSnapshot | null,
): snapshot is PaqadHudSnapshot & { table: PaqadHighTable } {
  return showsTable(snapshot) && !snapshot.table.skipped;
}

export function ledgerTabs(snapshot: PaqadHudSnapshot | null, shown: PaqadPaneTab): LedgerTab[] {
  const tabs = LEDGER_TABS.filter((tab) => tab.id !== 'table' || tableSat(snapshot));
  const picked = tabs.some((tab) => tab.id === shown) ? shown : 'trust';
  const width = `${Math.floor(100 / tabs.length)}%`;
  return tabs.map((tab) => ({ ...tab, width, shown: tab.id === picked }));
}

export type { TableSitting };

function tableIsLive(snapshot: PaqadHudSnapshot | null): boolean {
  if (!snapshot || !tableSat(snapshot)) return false;
  const chief = chiefState(snapshot);
  return (
    chief === 'ruling' ||
    chief === 'needs' ||
    (snapshot.table?.seats ?? []).some((seat) => seat.state === 'writing')
  );
}

export function ledgerAutoTab(
  snapshot: PaqadHudSnapshot | null,
  shown: PaqadPaneTab,
  sitting: TableSitting | null,
): { tab: PaqadPaneTab | null; sitting: TableSitting | null } {
  if (!tableIsLive(snapshot)) return { tab: null, sitting: null };
  const key = snapshot?.change?.dir ?? 'change';
  const current =
    sitting && sitting.key === key ? sitting : { key, forwarded: false, picked: false };
  if (current.forwarded || current.picked || shown === 'table') {
    return {
      tab: null,
      sitting: { ...current, forwarded: current.forwarded || shown === 'table' },
    };
  }
  return { tab: 'table', sitting: { ...current, forwarded: true } };
}

export function ledgerSubtitle(snapshot: PaqadHudSnapshot | null): string {
  const chip = changeChip(snapshot);
  return chip ? `What happened, with proof  ·  ${chip}` : 'What happened, with proof';
}

export function unreadableLight(reason: Unreadable): LightView {
  return { signal: 'attention', title: UNREADABLE, why: UNREADABLE_WHY[reason] };
}

export interface ClaimRow {
  key: string;
  id: string | null;
  label: string;
  sub: string;
  word: string;
  tone: WordTone;
}

export interface ClaimsView {
  rows: ClaimRow[];
  more: string | null;
  empty: string | null;
}

export const CLAIMS_LATER =
  'Claims appear when the spec is frozen. Each one is listed with the test that proves it.';
export const CLAIMS_LIMIT = 4;

function specFrozen(snapshot: PaqadHudSnapshot): boolean {
  const spec = snapshot.stages.find((stage) => stage.stage === 'specification');
  return spec?.state === 'complete' || spec?.state === 'redone';
}

export const MAPPED_WORDS = 'Has a test, not run yet';

export function claimsView(snapshot: PaqadHudSnapshot): ClaimsView {
  const rows: ClaimRow[] = snapshot.claims.map((claim, index) => {
    const proven = claim.verdict === 'pass';
    const fix = claim.kind === 'fix-proof';
    return {
      key: claim.id ?? `${claim.kind}-${index}`,
      id: claim.id,
      label: claim.label,
      sub: claim.test_id ?? 'No proving test recorded',
      word: proven ? 'Proven' : claim.verdict === 'mapped' ? MAPPED_WORDS : 'Not proven',
      tone: proven ? 'pass' : fix ? 'cau' : 'dim',
    };
  });
  if (rows.length === 0) {
    return {
      rows,
      more: null,
      empty: specFrozen(snapshot) ? 'No claim recorded for this change.' : CLAIMS_LATER,
    };
  }
  const rest = rows.slice(CLAIMS_LIMIT);
  const more =
    rest.length === 0
      ? null
      : rest.every((row) => row.word === 'Proven')
        ? `+${rest.length} more, all proven`
        : `+${rest.length} more`;
  return { rows: rows.slice(0, CLAIMS_LIMIT), more, empty: null };
}

export interface FlagRow {
  id: string;
  text: string;
}

export {
  GATE_STATE_WORDS,
  NOTHING_TO_PROVE,
  STEP_MARKS,
  type StepMark,
  type TrustGateRow,
  type TrustStage,
  type TrustStep,
} from './trust';

export interface SignoffRecord {
  words: string;
  link: { href: string; text: string } | null;
}

export interface TrustView {
  signoff?: SignoffRecord;
  empty: string | null;
  note: string | null;
  stages: TrustStage[];
  gates: TrustGateRow[];
  repairs: string | null;
  ledgerNote: string | null;
  gaps: string[];
  claims: ClaimsView;
  flags: FlagRow[];
}

export function ruleStopHistory(snapshot: PaqadHudSnapshot): FlagRow | null {
  const block = snapshot.last_block;
  if (!block) return null;
  const stopped = block.file ? `A rule stopped an edit to ${block.file}` : 'A rule stopped an edit';
  return {
    id: 'rule-stop',
    text: block.used === true ? `${stopped}. It was answered.` : `${stopped}.`,
  };
}

const SIGNOFF_RECORD_WORDS: Readonly<Record<PaqadSignoffState, string>> = {
  pending: 'not started',
  writing: 'writing the design',
  awaiting_sign_off: 'waiting for sign-off',
  approved: 'signed off',
  changes_requested: 'changes requested',
  skipped: 'skipped',
  not_posted: 'not posted',
};

export function signoffRecord(snapshot: PaqadHudSnapshot): SignoffRecord | null {
  const signoff = snapshot.active ? snapshot.signoff : null;
  if (!signoff) return null;
  return {
    words: `Design sign-off for ${signoff.ref ?? 'this change'}: ${SIGNOFF_RECORD_WORDS[signoff.state]}`,
    link: signoff.link ? { href: signoff.link, text: 'Open the design record' } : null,
  };
}

export function trustView(snapshot: PaqadHudSnapshot, now: number | null = null): TrustView {
  const active = snapshot.active;
  const history = active ? ruleStopHistory(snapshot) : null;
  const proof = active ? (snapshot.proof ?? null) : [];
  const stages =
    proof && proof.some(stageHasEvidence)
      ? proof.map((entry) => {
          const runs = stageRuns(
            snapshot.stages.find((record) => record.stage === entry.stage),
            now,
          );
          return {
            ...trustStage(entry),
            span: runs ? [runs.runs, runs.span].filter(Boolean).join(' · ') : null,
          };
        })
      : [];
  const gates = active && proof === null ? oldRecordGates(snapshot) : [];
  const claims = claimsView(snapshot);
  const signoff = signoffRecord(snapshot);
  const view: TrustView = {
    ...(signoff ? { signoff } : {}),
    empty: null,
    note: gates.length > 0 ? OLD_RECORD_NOTE : null,
    stages,
    gates,
    repairs: active ? repairLine(snapshot) : null,
    ledgerNote: active ? (snapshot.ledger_note ?? null) : null,
    gaps: active ? recordGapLines(snapshot) : [],
    claims,
    flags: active
      ? [
          ...snapshot.flags.map((flag) => ({ id: flag.id, text: flag.text })),
          ...(history ? [history] : []),
        ]
      : [],
  };
  const nothing =
    signoff === null &&
    stages.length === 0 &&
    gates.length === 0 &&
    view.repairs === null &&
    view.gaps.length === 0 &&
    claims.rows.length === 0 &&
    view.flags.length === 0;
  return nothing ? { ...view, empty: NOTHING_TO_PROVE } : view;
}

export interface SeatRow {
  role: string;
  initials: string;
  words: string;
  picked: boolean;
  word: string;
  tone: WordTone;
  details: string[];
}

export interface ChiefView {
  state: ChiefState;
  light: LightView;
  conflicts: Array<{ target: string; recommendation: string; rationale: string }>;
  answer: string | null;
}

export interface TableView {
  ring: TableSeat[];
  chief: ChiefState;
  alt: string;
  summary: { notes: string; findings: string; picked: string; standing: string };
  chiefView: ChiefView;
  seats: SeatRow[];
  shaped: {
    rows: Array<{ id: string; line: string; who: string }>;
    total: string | null;
    empty: string | null;
  };
}

export const ANSWER_TOAST =
  'Type your answers in the chat. The questions are in the High Table tab.';
export const SHAPED_LATER =
  'Shown when the spec is frozen: each spec line with the experts who shaped it.';

function spanClock(from: string | null, to: string | null): string | null {
  const ms = Date.parse(to ?? '') - Date.parse(from ?? '');
  return Number.isFinite(ms) && ms >= 0 ? formatClock(ms) : null;
}

function seatTokens(seat: PaqadSeat): number | null {
  return seat.tokens?.total ?? seat.tokens_estimate ?? null;
}

function seatDetails(seat: PaqadSeat): string[] {
  const details: string[] = [];
  if (seat.reason) details.push(seat.reason);
  if (seat.tokens_estimate !== null && seat.budget_tokens !== null) {
    details.push(
      `Note: ~${formatTokens(seat.tokens_estimate)} tokens of its ${formatTokens(seat.budget_tokens)} budget`,
    );
  }
  if (seat.tokens) {
    details.push(
      `Run: ${formatTokens(seat.tokens.total)} tokens (${seat.tokens_exact ? 'exact, from Claude Code' : 'estimate'})`,
    );
  }
  if (seat.track_record) details.push(trackRecordWords(seat.track_record));
  return details;
}

function seatRow(seat: PaqadSeat): SeatRow {
  const took = spanClock(seat.started_at, seat.done_at);
  const findings = seat.findings === null ? null : plural(seat.findings, 'finding', 'findings');
  const done = [took, findings].filter((part): part is string => part !== null).join(' · ');
  return {
    role: seat.role,
    initials: roleInitials(seat.role),
    words: roleWords(seat.role),
    picked: seat.origin !== 'standing',
    word:
      seat.state === 'writing' ? 'writing…' : seat.state === 'seated' ? 'seated' : done || 'done',
    tone: seat.state === 'writing' ? 'acc' : 'dim',
    details: seatDetails(seat),
  };
}

function chiefLight(table: PaqadHighTable, state: ChiefState): LightView {
  const chief = table.chief;
  const tokens =
    chief && seatTokens(chief) !== null ? `${formatTokens(seatTokens(chief)!)} tokens` : null;
  const ruledIn = chief ? spanClock(chief.started_at, chief.done_at) : null;
  const join = (...parts: Array<string | null>) =>
    parts.filter((part): part is string => part !== null).join(' · ') || null;
  if (state === 'needs') {
    return {
      signal: 'attention',
      title: WAITING_FOR_ANSWER,
      why: join(ruledIn ? `Ruled in ${ruledIn}` : null, tokens),
    };
  }
  if (state === 'done') {
    const answers = table.rulings?.settled.length ?? 0;
    return {
      signal: 'idle',
      title: 'Approved',
      why: join(
        answers > 0
          ? `After your ${plural(answers, 'answer', 'answers')}`
          : ruledIn
            ? `Ruled in ${ruledIn}`
            : null,
        tokens,
      ),
    };
  }
  if (state === 'ruling') {
    return { signal: 'accent', title: 'Ruling', why: 'The chief architect is reading the notes.' };
  }
  return {
    signal: 'idle',
    title: 'Listening',
    why: `Rules when all ${plural(table.seats.length, 'note has', 'notes have')} landed`,
  };
}

const COUNTED_LINE = /^(?:FR|AC)-\d+$/;

function specLines(specText: string | null): { lines: Map<string, string>; counted: Set<string> } {
  const lines = new Map<string, string>();
  const counted = new Set<string>();
  for (const raw of (specText ?? '').split(/\r?\n/)) {
    const match = /^\s*-\s+([A-Z]+-\d+):\s*(.*)$/.exec(raw);
    if (!match) continue;
    if (COUNTED_LINE.test(match[1]!)) counted.add(match[1]!);
    if (!lines.has(match[1]!)) lines.set(match[1]!, match[2]!);
  }
  return { lines, counted };
}

export function tableView(snapshot: PaqadHudSnapshot, specText: string | null): TableView | null {
  if (!tableSat(snapshot)) return null;
  const table = snapshot.table;
  const state = chiefState(snapshot);
  const seats = table.seats;
  const done = seats.filter((seat) => seat.state === 'done' || seat.late).length;
  const findings = seats.reduce((sum, seat) => sum + (seat.findings ?? 0), 0);
  const standing = seats.filter((seat) => seat.origin === 'standing').length;
  const open = snapshot.decisions.pending.filter(
    (decision) => decision.category === CONFLICT_CATEGORY,
  ).length;
  const { lines, counted } = specLines(specText);
  const shapedCount = new Set(
    table.credits.map((credit) => credit.line).filter((line) => counted.has(line)),
  ).size;
  return {
    ring: seats.map((seat) => ({
      label: roleInitials(seat.role),
      state: seat.late ? 'late' : seat.state,
      findings: seat.findings,
    })),
    chief: state,
    alt: `${done} of ${seats.length} notes landed`,
    summary: {
      notes: `${done} of ${plural(seats.length, 'note', 'notes')}`,
      findings: plural(findings, 'finding', 'findings'),
      picked: `${seats.length - standing} picked for this change`,
      standing: `${standing} always at the table`,
    },
    chiefView: {
      state,
      light: chiefLight(table, state),
      conflicts: state === 'needs' ? (table.rulings?.conflicts ?? []) : [],
      answer:
        state === 'needs'
          ? `Walk me through the ${plural(Math.max(open, 1), 'open decision', 'open decisions')}`
          : null,
    },
    seats: seats.map(seatRow),
    shaped: {
      rows: table.credits.map((credit) => ({
        id: credit.line,
        line: lines.get(credit.line) ?? credit.line,
        who: credit.roles.map(roleInitials).join(' · '),
      })),
      total:
        table.credits.length > 0 && counted.size > 0
          ? `${shapedCount} of ${counted.size} spec lines came from an expert note.`
          : null,
      empty: table.credits.length === 0 ? SHAPED_LATER : null,
    },
  };
}

export interface DeliveryJob {
  name: string;
  time: string;
  tone: WordTone;
  share: number | null;
}

export interface DeliveryBlock {
  label: DeliveryLabel;
  state: ShipState;
  title: string;
  sub: string | null;
  right: string | null;
  note: string | null;
  reached: boolean;
  commits: Array<{ sha: string; subject: string; own: boolean }>;
  jobs: DeliveryJob[];
  link: { href: string; text: string } | null;
  ci: number | null;
}

const JOB_PASSED = new Set(['SUCCESS', 'NEUTRAL', 'SKIPPED']);
const JOB_FAILED = new Set(['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED']);
const JOB_QUEUED = new Set(['QUEUED', 'PENDING', 'EXPECTED', 'WAITING', 'REQUESTED']);

function deliveryJob(job: PaqadCiJob): DeliveryJob {
  const outcome = (job.conclusion ?? job.status ?? '').toUpperCase();
  const took = spanClock(job.started_at, job.completed_at);
  if (JOB_FAILED.has(outcome))
    return { name: job.name, time: took ?? 'failed', tone: 'fail', share: 1 };
  if (JOB_PASSED.has(outcome))
    return { name: job.name, time: took ?? 'done', tone: 'dim', share: 1 };
  if (JOB_QUEUED.has(outcome)) return { name: job.name, time: 'queued', tone: 'dim', share: 0 };
  return { name: job.name, time: 'running', tone: 'dim', share: null };
}

function ciSub(ship: NonNullable<PaqadHudSnapshot['ship']>): string {
  const ci = ship.ci_detail;
  if (!ci || ci.total === null || ci.total === 0) return 'Runs on the pull request';
  if (ci.state === 'pass') {
    const started = ci.jobs.map((job) => Date.parse(job.started_at ?? '')).filter(Number.isFinite);
    const ended = ci.jobs.map((job) => Date.parse(job.completed_at ?? '')).filter(Number.isFinite);
    const took =
      started.length > 0 && ended.length > 0
        ? formatClock(Math.max(...ended) - Math.min(...started))
        : null;
    return `All ${ci.total} checks passed${took ? ` in ${took}` : ''}`;
  }
  return `${ci.done ?? 0} of ${ci.total} checks green`;
}

function blockText(
  snapshot: PaqadHudSnapshot,
  label: DeliveryLabel,
  state: ShipState,
): {
  title: string;
  sub: string | null;
  right: string | null;
} {
  const ship = snapshot.ship;
  switch (label) {
    case 'Edit': {
      const files = ship?.files_changed ?? null;
      return {
        title: 'Edit',
        sub:
          state === 'todo'
            ? 'Code changes start after the spec is frozen.'
            : files === null
              ? null
              : `${plural(files, 'file', 'files')} changed by this change`,
        right: null,
      };
    }
    case 'Commit': {
      const own = ship?.commits ?? 0;
      return {
        title: 'Commits',
        sub: own > 0 ? `${own} by this change` : 'None of yours yet',
        right: null,
      };
    }
    case 'Push':
      return {
        title: 'Push',
        sub:
          ship?.push === 'failed'
            ? 'The push failed'
            : ship?.push === 'pushed'
              ? `${ship.push_branch ?? 'The branch'} on origin`
              : 'After Checks and Docs',
        right: null,
      };
    case 'PR': {
      const opened = ship !== null && ship.pr_number !== null;
      const size =
        ship && ship.pr_additions !== null && ship.pr_deletions !== null
          ? `+${ship.pr_additions} −${ship.pr_deletions}`
          : null;
      return {
        title: 'Pull request',
        sub: opened
          ? `#${ship.pr_number}${ship.pr_title ? ` ${ship.pr_title}` : ''}`
          : 'Opened after the push',
        right: opened ? size : null,
      };
    }
    case 'CI':
      return { title: 'CI', sub: ship ? ciSub(ship) : 'Runs on the pull request', right: null };
  }
}

const NOT_RECORDED_START = 'where this change started was not recorded.';

function startNote(snapshot: PaqadHudSnapshot): string | null {
  const start = snapshot.start_point;
  if (!start) return null;
  if (start.source === 'fallback-base') {
    return `Measured from ${start.base ?? 'the base branch'}: ${NOT_RECORDED_START}`;
  }
  if (start.source === 'fork-point') {
    return `Measured from where the branch left ${start.base ?? 'its base'}: ${NOT_RECORDED_START}`;
  }
  return start.files_known
    ? null
    : 'The files already in the tree when this change opened are unknown.';
}

function earlierWorkNote(snapshot: PaqadHudSnapshot): string | null {
  const earlier = snapshot.ship?.inherited_count ?? 0;
  if (earlier <= 0) return null;
  return `Branch already had ${plural(earlier, 'commit', 'commits')} from earlier work.`;
}

function blockNote(snapshot: PaqadHudSnapshot, label: DeliveryLabel): string | null {
  if (label === 'Edit') return startNote(snapshot);
  if (label === 'Commit') return earlierWorkNote(snapshot);
  return null;
}

export interface DeliveryView {
  blocks: DeliveryBlock[];
  empty: string | null;
}

export function deliveryView(
  snapshot: PaqadHudSnapshot,
  live: LiveState = IDLE_LIVE,
): DeliveryView {
  if (!snapshot.active && !snapshot.closed_change) {
    return { blocks: [], empty: 'Nothing to deliver yet.' };
  }
  const strip = deliveryStrip(snapshot, live);
  const ship = snapshot.ship;
  return {
    empty: null,
    blocks: strip.steps.map(({ label, state }) => {
      const text = blockText(snapshot, label as DeliveryLabel, state);
      const reached = state !== 'todo';
      return {
        label: label as DeliveryLabel,
        state,
        ...text,
        note: blockNote(snapshot, label as DeliveryLabel),
        reached,
        commits:
          label === 'Commit' && ship
            ? ship.commit_list
                .filter((commit) => commit.own !== false)
                .map((commit) => ({
                  sha: commit.sha.slice(0, 7),
                  subject: commit.subject,
                  own: true,
                }))
            : [],
        jobs:
          label === 'CI' && reached && ship?.ci_detail ? ship.ci_detail.jobs.map(deliveryJob) : [],
        link:
          label === 'PR' && ship?.pr_state === 'open' && ship.pr_url
            ? { href: ship.pr_url, text: `Open ${prName(snapshot)}` }
            : null,
        ci: label === 'CI' ? strip.ci : null,
      };
    }),
  };
}

export function localTime(at: string): string {
  const date = new Date(Date.parse(at));
  return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}

const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

export function recordTime(at: string): string {
  const date = new Date(Date.parse(at));
  if (Number.isNaN(date.getTime())) return '';
  return `${MONTHS[date.getMonth()]} ${date.getDate()} ${localTime(at)}`;
}

function recordWords(snapshot: PaqadHudSnapshot, entry: PaqadTimelineEntry): string | null {
  const stage = entry.stage ? stageWord(entry.stage) : 'A stage';
  const count = entry.count ?? 0;
  switch (entry.kind) {
    case 'open': {
      if (entry.note) return `Reopened: ${entry.note}`;
      const issue = entry.issue
        ? /^\d+$/.test(entry.issue)
          ? `#${entry.issue}`
          : entry.issue
        : null;
      const name = [issue, entry.title].filter(Boolean).join(' ');
      const lane = entry.lane ? `${entry.lane} lane` : null;
      const what = [name || null, lane].filter(Boolean).join(', ');
      return what ? `Change opened: ${what}` : 'Change opened';
    }
    case 'close':
      return entry.note ? `Closed: ${entry.note}` : 'Closed';
    case 'stage_start':
      return entry.own_agent ? `${stage} started in its own agent` : `${stage} started`;
    case 'stage_end':
      return entry.duration_ms !== undefined
        ? `${stage} done in ${formatClock(entry.duration_ms)}`
        : `${stage} done`;
    case 'rules_loaded':
      return `Rules loaded: ${count} apply`;
    case 'table_seated':
      return `High Table seated: ${entry.picked ?? 0} picked, ${entry.standing ?? 0} standing`;
    case 'notes_landed':
      return `All ${plural(count, 'note', 'notes')} landed, ${plural(entry.findings ?? 0, 'finding', 'findings')}`;
    case 'chief_ruled': {
      const took = entry.duration_ms !== undefined ? ` in ${formatClock(entry.duration_ms)}` : '';
      return `Chief ruled${took}${entry.verdict ? `: ${entry.verdict}` : ''}`;
    }
    case 'decision_opened': {
      const name = entry.decision ?? 'A decision';
      return entry.subject ? `${name} opened: ${entry.subject}` : `${name} opened for you`;
    }
    case 'decision_answered':
      return entry.decision ? `You answered ${entry.decision}` : 'You answered a decision';
    case 'decisions_opened':
      return `${plural(count, 'decision', 'decisions')} opened for you`;
    case 'decisions_answered':
      return `You answered ${plural(count, 'decision', 'decisions')}`;
    case 'spec_frozen':
      return `Spec frozen: ${plural(entry.requirements ?? 0, 'requirement', 'requirements')}, ${plural(entry.criteria ?? 0, 'criterion', 'criteria')}`;
    case 'commit':
      return `Commit ${(entry.sha ?? '').slice(0, 5)}: ${entry.subject ?? ''}`.trim();
    case 'push':
      return entry.state === 'failed'
        ? 'The push failed'
        : `Pushed ${plural(count, 'commit', 'commits')}`;
    case 'pr_opened':
      return entry.number !== undefined
        ? `PR #${entry.number} opened`
        : `${prName(snapshot)} opened`;
    case 'ci':
      return entry.state === 'fail'
        ? 'CI failed'
        : `CI green on all ${plural(count, 'job', 'jobs')}`;
    default:
      return null;
  }
}

export interface RecordView {
  rows: Array<{ key: string; time: string; text: string }>;
  empty: string | null;
}

export const RECORD_LATER = 'The record starts when the change opens.';
export const RECORD_NOTHING_YET = 'No decisions or stage events for this change yet.';

const RECORDED_WORK = /^(stage_|decision)/;

function newestFirst(timeline: readonly PaqadTimelineEntry[]) {
  const timeOf = (entry: PaqadTimelineEntry) => {
    const at = Date.parse(entry.at);
    return Number.isNaN(at) ? Number.NEGATIVE_INFINITY : at;
  };
  return timeline
    .map((entry, index) => ({ entry, index, at: timeOf(entry) }))
    .sort((a, b) => (a.at === b.at ? b.index - a.index : b.at - a.at));
}

export function recordView(snapshot: PaqadHudSnapshot): RecordView {
  if (!snapshot.active) return { rows: [], empty: RECORD_LATER };
  if (!snapshot.timeline.some((entry) => RECORDED_WORK.test(entry.kind))) {
    return { rows: [], empty: RECORD_NOTHING_YET };
  }
  const rows = newestFirst(snapshot.timeline).flatMap(({ entry, index }) => {
    const text = recordWords(snapshot, entry);
    return text ? [{ key: `${entry.at}-${index}`, time: recordTime(entry.at), text }] : [];
  });
  return { rows, empty: rows.length === 0 ? RECORD_NOTHING_YET : null };
}

export interface CostMeter {
  key: string;
  name: string;
  right: string;
  share: number;
}

export interface CostView {
  total: string;
  money: string;
  note: string;
  live: string | null;
  stages: CostMeter[];
  table: CostMeter[];
  notCounted: string;
}

export const NOT_COUNTED_WORDS =
  'Not counted: your own typing time, other sessions, and the HUD itself (0 tokens).';
export const COUNTING_WORDS = 'Counting: this turn is still running.';
export const COST_LATER = 'Nothing counted yet. The cost starts when the change opens.';

function lineRight(line: PaqadCostLine): string {
  const tokens = formatTokens(line.tokens.total);
  return line.usd === null ? tokens : `${tokens} · ${formatUsd(line.usd)}`;
}

function meters(entries: Array<{ key: string; name: string; line: PaqadCostLine }>): CostMeter[] {
  const largest = Math.max(0, ...entries.map((entry) => entry.line.tokens.total));
  return entries.map((entry) => ({
    key: entry.key,
    name: entry.name,
    right: lineRight(entry.line),
    share: largest > 0 ? entry.line.tokens.total / largest : 0,
  }));
}

function costStageName(stage: string, table: boolean): string {
  if (stage === 'orchestrator') return 'Between stages';
  if (stage === 'specification' && table) return 'Spec, with the High Table';
  return stageWord(stage);
}

export function costMoney(cost: PaqadCost): string {
  return cost.usd === null
    ? cost.unpriced_models.length > 0
      ? `No price for ${cost.unpriced_models.join(', ')}`
      : 'No money figure'
    : `~${formatUsd(cost.usd)} estimate at API prices · ${cost.price_table_date} prices`;
}

export function costView(snapshot: PaqadHudSnapshot): CostView | null {
  const cost = snapshot.active ? snapshot.cost : null;
  if (!cost) return null;
  const sat = tableSat(snapshot);
  const scope = cost.scope === 'whole-session' ? WHOLE_SESSION_SENTENCE : CHANGE_SCOPE_SENTENCE;
  const counting = stillCounting(cost);
  return {
    total: counting ? COST_COUNTING : `${formatTokens(cost.tokens.total)} tokens`,
    money: counting ? COST_COUNTING_MONEY : costMoney(cost),
    note: [cost.plan_note, scope].filter((part) => part.length > 0).join(' '),
    live: cost.live ? COUNTING_WORDS : null,
    stages: meters(
      cost.by_stage.map((line) => ({
        key: line.stage,
        name: costStageName(line.stage, sat),
        line,
      })),
    ),
    table: sat
      ? meters([
          ...(cost.chief ? [{ key: 'chief', name: 'Chief architect', line: cost.chief }] : []),
          ...cost.by_expert.map((line) => ({ key: line.role, name: roleWords(line.role), line })),
        ])
      : [],
    notCounted: NOT_COUNTED_WORDS,
  };
}

export interface WorkflowLedger {
  light: LightView;
  total: string;
  money: string | null;
  note: string;
}

export function workflowLedger(snapshot: PaqadHudSnapshot, route: string): WorkflowLedger {
  const words = workflowWords(route);
  const workflow = snapshot.workflow?.name === route ? snapshot.workflow : null;
  return {
    light: {
      signal: 'accent',
      title: capitalised(words),
      why: 'No change in this session, so there is nothing to prove. paqad shows what this workflow cost.',
    },
    total:
      workflow?.tokens === null || !workflow
        ? 'counting'
        : `${formatTokens(workflow.tokens)} tokens`,
    money:
      workflow && workflow.usd !== null
        ? `~${formatUsd(workflow.usd)} estimate at API prices`
        : null,
    note: `Counted from when this ${words} started in this session. Nothing from other sessions or earlier changes.`,
  };
}

export type DesktopLedger =
  | { kind: 'none' }
  | { kind: 'no-change'; words: string }
  | { kind: 'unreadable'; light: LightView }
  | { kind: 'workflow'; view: WorkflowLedger }
  | {
      kind: 'feature';
      subtitle: string;
      tabs: LedgerTab[];
      shown: PaqadPaneTab;
      trust: TrustView;
      table: TableView | null;
      delivery: DeliveryView;
      record: RecordView;
      cost: CostView | null;
    };

export interface DesktopLedgerInput {
  snapshot: PaqadHudSnapshot | null;
  unbound: boolean;
  unavailable: boolean;
  tab: PaqadPaneTab | null;
  now: number;
  specText: string | null;
  live?: LiveState;
}

export function buildDesktopLedger(input: DesktopLedgerInput): DesktopLedger {
  const unreadable = unreadableReason(input);
  if (unreadable) return { kind: 'unreadable', light: unreadableLight(unreadable) };
  const snapshot = input.snapshot;
  if (!snapshot) return { kind: 'none' };
  const route = statusEntryRoute(snapshot);
  if (route) return { kind: 'workflow', view: workflowLedger(snapshot, route) };
  if (readyFeatureRoute(snapshot)) return { kind: 'no-change', words: NO_CHANGE };
  const tabs = ledgerTabs(snapshot, input.tab ?? 'trust');
  return {
    kind: 'feature',
    subtitle: ledgerSubtitle(snapshot),
    tabs,
    shown: tabs.find((tab) => tab.shown)!.id,
    trust: trustView(snapshot, input.now),
    table: tableView(snapshot, input.specText),
    delivery: deliveryView(snapshot, input.live),
    record: recordView(snapshot),
    cost: costView(snapshot),
  };
}

export function ledgerIsLive(ledger: DesktopLedger): boolean {
  if (ledger.kind !== 'feature') return false;
  const writing = ledger.table?.seats.some((seat) => seat.word === 'writing…') ?? false;
  const ruling = ledger.table?.chief === 'ruling' || ledger.table?.chief === 'needs';
  const running = ledger.delivery.blocks.some((block) => block.state === 'run');
  return writing || ruling || running;
}

export const UPDATE_PLAN_TOOL = 'mcp__ccd_turn__update_plan';

export const TASK_LIST_SENTENCES = {
  full: 'paqad task list: Plan, Spec, Build, Review, Checks, Docs. Start each item with that word, in this order. Items before Plan (read the ticket) and after Docs (open the PR) are fine.',
  fast: 'paqad task list: Plan, Build, Checks. Start each item with that word, in this order. Items before Plan and after Checks are fine.',
} as const;

const TASK_WORDS = {
  full: ['plan', 'spec', 'build', 'review', 'checks', 'docs'],
  fast: ['plan', 'build', 'checks'],
} as const;

const PASSING_ACTIONS = new Set(['advance', 'done', 'start']);

export function taskListSentence(lane: string | null): string {
  return lane === 'fast' ? TASK_LIST_SENTENCES.fast : TASK_LIST_SENTENCES.full;
}

function itemContent(item: unknown): string {
  if (typeof item === 'string') return item;
  const content =
    typeof item === 'object' && item !== null ? (item as { content?: unknown }).content : null;
  return typeof content === 'string' ? content : '';
}

export function refusesTaskList(lane: string | null, call: Record<string, unknown>): boolean {
  if (!Array.isArray(call.items)) return false;
  for (const key of ['action', 'op', 'type']) {
    const value = call[key];
    if (typeof value === 'string' && PASSING_ACTIONS.has(value.trim().toLowerCase())) return false;
  }
  const words = lane === 'fast' ? TASK_WORDS.fast : TASK_WORDS.full;
  let next = 0;
  for (const item of call.items) {
    const content = itemContent(item).trim().toLowerCase();
    if (next < words.length && content.startsWith(words[next]!)) next += 1;
  }
  return next < words.length;
}