import { atom, read, update } from 'claude-code'
import type { Register } from 'claude-code'

import type { CockpitStatus, SkillsReport } from '../types'
import { checkGrant } from './lib/grants.mjs'
import { parseAccept, pressNote, planAcceptance, planGrant, writerOf } from './lib/commands.mjs'
import { PATH_KEYS, classifyTool, denyMerge, denyMessage, findingsFor, isFileWriteTool, looseDeny, readsAsApproval, wsPortsOf } from './lib/guard.mjs'
import { recordPath, resolveInitiative } from './lib/initiative.mjs'
import { detectLayout } from './lib/layout.mjs'
import { parseProfile } from './lib/profile.mjs'
import { diffSkills, parseDescription, linkTarget, NAME_RE } from './lib/skills.mjs'
import { PROCESS_LIMIT_MS, isFresh, parseFactsOutput, sliceRows, summarize } from './lib/session-facts.mjs'
import { clean, formatStatus } from './lib/status-line.mjs'
import { SECTION_ID, buildSection } from './lib/prompt-section.mjs'
import { PROBES, aheadProbe, interpretGit } from './lib/probes.mjs'
import { describeMode, detectMode, slicePolicy } from './lib/mode.mjs'
import { UNPUSHED_PROBES, cloudBandText, interpretUnpushed } from './lib/unpushed.mjs'
import { HANDOVER_PROBES, buildPlan, countSince, interpretSince, notePaths, parseNote, planStillGood, renderNote, resumeText, sinceProbes, subjectsProbe } from './lib/handover.mjs'
import { carryOut } from './lib/handover-run.mjs'
import { REVIEWER_AGENT, REVIEWER_PROMPT, REVIEWER_TOOLS, REVIEWER_TYPE, buildReviewPrompt, offerText, parseVerdict, pickReviewerModel, planEval } from './lib/review.mjs'
import { ASK_LIMIT_MS, decide, parseRoute, routeUrl, shouldRoute, subagentModel, switchNote } from './lib/route.mjs'

const status = atom({ plugin: 'pmos-cockpit', key: 'status' } as const, null)
const isHidden = atom({ plugin: 'pmos-cockpit', key: 'isHidden' } as const, false)
// A run the person's last message may have accepted. It is an offer to press, never a record.
const offer = atom({ plugin: 'pmos-cockpit', key: 'offer' } as const, null)
// What the status line, the band, the prompt section and the slice share: one read of the checkout and
// of the ladder. `factsCache` holds the last ladder read, for one initiative at one commit.
const view = atom({ plugin: 'pmos-cockpit', key: 'view' } as const, null)
const factsCache = atom({ plugin: 'pmos-cockpit', key: 'factsCache' } as const, null)
// Where this session runs (M12). Read from two environment variables when the mod loads and kept here;
// nothing a tool call does afterwards moves it. It changes what is drawn, never what is guarded.
const modeAtom = atom({ plugin: 'pmos-cockpit', key: 'mode' } as const, null)
// Continuity (M13). The plan `/pmos-handover` showed, waiting for a person's confirmation; it holds the
// facts of the checkout it was made for. The resume summary read at session start, and whether the
// person dismissed it. None of the three is ever given to the model.
const handoverPlan = atom({ plugin: 'pmos-cockpit', key: 'handoverPlan' } as const, null)
const resume = atom({ plugin: 'pmos-cockpit', key: 'resume' } as const, null)
const resumeGone = atom({ plugin: 'pmos-cockpit', key: 'resumeGone' } as const, false)
// The independent evaluator (M14). `review` is the one reviewer in flight; `reviewOffer` is its answer,
// held until a person presses Record or types /pmos-record-review. Neither is ever written to a ledger.
const review = atom({ plugin: 'pmos-cockpit', key: 'review' } as const, null)
const reviewOffer = atom({ plugin: 'pmos-cockpit', key: 'reviewOffer' } as const, null)
// Model routing (prompt-model-routing, Run 3). On by default for the session; /pmos-route off stops it.
// `told` keeps the "why nothing was switched" toast to once a session.
const routing = atom({ plugin: 'pmos-cockpit', key: 'routing' } as const, { on: true, told: false })
// The width the person's terminal draws the band at, seen at the last render. Lost on a reload, which
// only returns the status line to its default width.
let lastColumns = 0

// ── helpers for the guards and commands ───────────────────────────────────────────────────────────
const out = (r: any): string => (r && typeof r.stdout === 'string' ? r.stdout.trim() : '')
async function sh($: any, argv: string[], init?: any): Promise<{ exitCode: number; stdout: string; stderr: string }> {
  try { return await $.process.run(argv, init) } catch (e: any) { return { exitCode: 1, stdout: '', stderr: String(e?.message ?? e) } }
}

// The workspace's ports for the PM-only route check: the defaults, plus the port of PMOS_ROUTE_URL and a pinned
// PMOS_WS_PORT from this session's environment. A product's own server on another port is not the workspace.
async function wsPorts($: any): Promise<number[]> {
  const env: Record<string, string | undefined> = {}
  for (const k of ['PMOS_ROUTE_URL', 'PMOS_WS_PORT']) { try { env[k] = await $.env.get(k) } catch { env[k] = undefined } }
  return wsPortsOf(env)
}

async function rootAndLayout($: any) {
  const root: string = await $.session.root()
  const layout = await detectLayout({ exists: (p: string) => $.fs.exists(p) }, root)
  return { root, layout: { ...layout, root } }
}

// What git says about the session's checkout. A linked worktree answers for itself.
async function gitFacts($: any, root: string) {
  const b = await sh($, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
  const branch = b.exitCode === 0 && out(b) !== '' ? out(b) : null
  const d = await sh($, ['git', 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
  const defaultBranch = d.exitCode === 0 && out(d) !== '' ? out(d).replace(/^origin\//, '') : null
  let commitsAhead: number | null = null
  if (branch !== null) {
    const c = await sh($, ['git', 'rev-list', '--count', `${defaultBranch ?? 'main'}..HEAD`], { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
    commitsAhead = c.exitCode === 0 && /^\d+$/.test(out(c)) ? Number(out(c)) : null
  }
  return { branch, defaultBranch, commitsAhead }
}

// Read the two variables once per load. The names are literals, as the engine requires.
async function readMode($: any): Promise<{ mode: string; signal: string | null }> {
  const get = async (name: 'CLAUDE_CODE_REMOTE' | 'PMOS_WORKSPACE'): Promise<string | undefined> => {
    try { return name === 'CLAUDE_CODE_REMOTE' ? await $.env.get('CLAUDE_CODE_REMOTE') : await $.env.get('PMOS_WORKSPACE') } catch { return undefined }
  }
  return detectMode({ remote: await get('CLAUDE_CODE_REMOTE'), workspace: await get('PMOS_WORKSPACE') })
}

const isPerson = (e: any): boolean => e?.origin?.kind === 'composer' || e?.origin?.kind === 'bridge'

// Ask the workspace for a prompt's route. The workspace holds the Jev key and asks Jev only when D84's
// gates are open; the mod never sees the key and reaches no host but the loopback address the workspace
// put in the terminal's environment. A terminal started outside the workspace has none: nothing is asked.
async function askRoute($: any, prompt: string, source: 'pm' | 'agent'): Promise<any> {
  let url: string | null = null
  try { url = routeUrl(await $.env.get('PMOS_ROUTE_URL')) } catch { url = null }
  if (url === null) return null
  try {
    const root: string = await $.session.root()
    const ask = $.http.fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ root, prompt, source, interactive: true }) })
      .then((r: any) => parseRoute(r?.status, r?.text)).catch(() => null)
    const late = $.clock.sleep(ASK_LIMIT_MS).then(() => null).catch(() => null)
    return await Promise.race([ask, late])
  } catch { return null }
}

async function readText($: any, path: string): Promise<string | null> {
  try { return (await $.fs.exists(path)) ? await $.fs.read(path) : null } catch { return null }
}

async function whoAmI($: any, root: string): Promise<string> {
  const r = await sh($, ['git', 'config', 'user.name'], { cwd: root })
  return r.exitCode === 0 && out(r) !== '' ? out(r) : 'pm'
}

// The local gate for one initiative. It reports what it ran, and what it cannot see. `fromGrant` is true
// when the id comes from the grant (Run 7) because the branch names none: the kit's gate then reads the
// id from `Initiative: <id>` in the PR body, the form the CI gate accepts in place of the branch name.
async function localGate($: any, layout: any, root: string, id: string | null, branch: string | null, fromGrant = false) {
  if (id === null) return { ran: false, failed: false, text: 'Local gate not run: the branch names no initiative, and neither does the grant.' }
  const lines: string[] = fromGrant ? [`Local gate run for initiative ${id}, named on the grant.`] : []
  let failed = false
  let ran = false
  const ws = layout.kind === 'kit' ? `${root}/pmos/scripts/workflow-state.js` : `${root}/scripts/workflow-state.js`
  if (await $.fs.exists(ws)) {
    const r = await sh($, ['node', ws, '--gate', id], { cwd: root })
    ran = true
    if (r.exitCode !== 0) failed = true
    lines.push(`workflow-state --gate ${id}: ${r.exitCode === 0 ? 'passed' : `FAILED. ${out(r).split('\n').slice(-3).join(' ')}`}`)
  }
  if (layout.kind === 'kit' && (await $.fs.exists(`${root}/pmos/scripts/gate-initiative.sh`))) {
    const r = await sh($, ['bash', `${root}/pmos/scripts/gate-initiative.sh`], { cwd: root, env: { HEAD_REF: branch ?? '', PR_BODY: fromGrant ? `Initiative: ${id}` : '', PR_LABELS: '', KIT_ROOT: 'pmos' } })
    ran = true
    if (r.exitCode !== 0) failed = true
    lines.push(`gate-initiative.sh: ${r.exitCode === 0 ? 'passed' : `FAILED. ${(out(r) || r.stderr).split('\n').slice(-2).join(' ')}`}`)
  }
  if (!ran) lines.push('Local gate not run: no gate script is installed here.')
  lines.push('This run does not see: anchors (gate-initiative.sh does not resolve them; CI does), or anything on a remote.')
  return { ran, failed, text: lines.join(' ') }
}

// Candidate artifact names for `npm publish`: the package.json beside the target, then the session root.
async function publishNames($: any, root: string, f: any): Promise<string[]> {
  const names: string[] = []
  const dirs = typeof f.target === 'string' && !f.target.startsWith('-') ? [f.target.startsWith('/') ? f.target : `${root}/${f.target}`] : []
  dirs.push(root)
  for (const d of dirs) {
    const text = await readText($, `${d}/package.json`)
    if (text === null) continue
    try { const n = JSON.parse(text).name; if (typeof n === 'string') names.push(n) } catch { /* not JSON: no name */ }
  }
  return [...new Set(names)]
}

// One tool call, judged. Returns a deny text, or null to let it through. It also spends the grants
// it relied on, so a single-use grant is used up before the action runs.
async function judge($: any, e: any): Promise<string | null> {
  const tool = String(e.tool)
  if (tool !== 'Bash' && !isFileWriteTool(tool) && !tool.startsWith('mcp__')) return null
  const { root, layout } = await rootAndLayout($)
  const needsBranch = (tool === 'Bash' && /\bpush\b/.test(String(e.command ?? ''))) || tool.startsWith('mcp__github')
  let facts = needsBranch ? await gitFacts($, root) : { branch: null, defaultBranch: null, commitsAhead: null }
  const ctx = { root, layout, branch: facts.branch, defaultBranch: facts.defaultBranch, wsPorts: await wsPorts($) }
  const settingsPath = `${root}/.claude/settings.json`
  const settingsText = isFileWriteTool(tool) ? await readText($, settingsPath) : null
  const findings = findingsFor(e, ctx, settingsText)
  // A path that is a link to a ledger is the ledger. Judge where each path really leads, as well.
  if (isFileWriteTool(tool)) {
    const real: any = { ...e }
    let moved = false
    for (const k of PATH_KEYS) {
      const p = e[k]
      if (typeof p !== 'string' || p === '') continue
      const abs = p.startsWith('/') ? p : `${root}/${p}`
      const st = await $.fs.stat(abs, { resolve: true }).catch(() => undefined)
      if (st?.realPath && st.realPath !== abs) { real[k] = st.realPath; moved = true }
    }
    if (moved) findings.push(...findingsFor(real, ctx, settingsText))
  }
  if (findings.length === 0) return null

  for (const kind of ['accept-grants', 'pm-act', 'ledger', 'governing', 'cloud']) {
    const f = findings.find((x) => x.kind === kind)
    if (f) return denyMessage(f, ctx)
  }
  const merges = findings.filter((x) => x.kind === 'merge')
  if (merges.length === 0) return null // an outward write: tool.check asks

  // the gate needs the branch, whatever the action was
  if (!needsBranch) facts = await gitFacts($, root)

  // the initiative the branch names, if any: a grant minted for another one never covers this action
  const initiative = resolveInitiative({
    branch: facts.branch,
    recordExists: facts.branch ? await $.fs.exists(recordPath(layout, facts.branch.replace(/^initiative\//, '')) ?? '/nonexistent') : null,
    commitsAhead: facts.commitsAhead,
  })

  const grantsFile = layout.stateRoot ? `${layout.stateRoot}/grants.jsonl` : null
  const text = grantsFile ? ((await readText($, grantsFile)) ?? '') : ''
  const now: number = await $.clock.now()
  const used: { id: string; action: string; initiative: string | null }[] = []
  for (const f of merges) {
    if (grantsFile === null) return denyMerge(f, 'there is no PMOS state here to hold a grant.')
    let checks: { kind: string; target: string }[]
    if (f.publish) {
      const names = await publishNames($, root, f)
      if (names.length === 0) return denyMerge({ ...f, target: null }, 'the artifact name could not be read from a package.json, so no grant can match.')
      checks = names.map((n) => ({ kind: 'publish', target: n }))
    } else {
      if (f.target === null || f.target === undefined) return denyMerge(f, `${f.action} names no PR number or branch, so no grant can match. Name it.`)
      checks = [{ kind: 'merge', target: String(f.target) }]
    }
    let hit: any = null
    let why = ''
    for (const c of checks) {
      const r = checkGrant(text, { ...c, branchInitiative: initiative.id }, now)
      if (r.ok) { hit = r.grant; break }
      why = r.reason
    }
    if (hit === null) return denyMerge(f.publish ? { ...f, target: checks[0].target } : f, `${why}`)
    used.push({ id: hit.id, action: `${f.action}${f.target ? ` ${f.target}` : ''}`, initiative: hit.initiative ?? null })
  }

  // every grant is live; now the local gate, which can still stop the action. It runs for the branch's
  // initiative, or else for the one the grants name. Grants that name different ones decide nothing.
  const named = [...new Set(used.map((u) => u.initiative).filter((x): x is string => x !== null))]
  if (initiative.id === null && named.length > 1) {
    return `Blocked by pmos-cockpit: the grants for this action name different initiatives (${named.join(', ')}), so there is no one initiative to run the local gate against. Ask the PM for grants that name one.`
  }
  const gateId = initiative.id ?? named[0] ?? null
  const gate = await localGate($, layout, root, gateId, facts.branch, initiative.id === null && gateId !== null)
  $.ui.toast(`pmos-cockpit: ${gate.text}`)
  if (gate.failed) return `Blocked by pmos-cockpit: the local gate failed, and a grant does not override it. ${gate.text}`

  const w = writerOf(layout, root)
  if (w === null) return denyMerge(merges[0], 'no writer is installed to record the use of the grant.')
  for (const u of used) {
    const r = await sh($, [...w.argv, 'grant-use', u.id, u.action], { cwd: root, env: w.env })
    if (r.exitCode !== 0) return `Blocked by pmos-cockpit: the use of grant ${u.id} could not be recorded (${(r.stderr || out(r)).trim()}), so the action is denied.`
  }
  return null
}

// Accepting a run: the same writer, whichever layout. Called only from a person's command or press.
async function writeAcceptance($: any, run: { run_id: string; initiative_id: string }, label: 'pass' | 'fail', note: string): Promise<{ ok: boolean; text: string }> {
  const { root, layout } = await rootAndLayout($)
  const plan = planAcceptance({ layout, root, run, label, note, pm: await whoAmI($, root), atIso: new Date(await $.clock.now()).toISOString() })
  if (!plan.ok) return { ok: false, text: plan.text }
  const r = await sh($, plan.argv, { cwd: root, env: plan.env })
  return { ok: r.exitCode === 0, text: out(r) || r.stderr.trim() }
}

async function runsOf($: any, layout: any): Promise<{ run_id: string; initiative_id: string }[]> {
  if (!layout.stateRoot || !layout.runsFile) return []
  const text = (await readText($, `${layout.stateRoot}/${layout.runsFile}`)) ?? ''
  const rows: any[] = []
  for (const l of text.split('\n')) { try { const r = JSON.parse(l); if (r && typeof r.run_id === 'string' && typeof r.initiative_id === 'string') rows.push(r) } catch { /* skipped */ } }
  return rows
}

async function acceptedIds($: any, layout: any): Promise<Set<string>> {
  const text = layout.stateRoot ? ((await readText($, `${layout.stateRoot}/acceptances.jsonl`)) ?? '') : ''
  const ids = new Set<string>()
  for (const l of text.split('\n')) { try { const r = JSON.parse(l); if (r && typeof r.run_id === 'string') ids.add(r.run_id) } catch { /* skipped */ } }
  return ids
}

// The newest run of the session's initiative that has no acceptance row, or null.
async function buildOffer($: any, message: string) {
  const { root, layout } = await rootAndLayout($)
  const facts = await gitFacts($, root)
  const id = facts.branch?.startsWith('initiative/') ? facts.branch.slice('initiative/'.length) : null
  if (id === null) return null
  const accepted = await acceptedIds($, layout)
  const runs = (await runsOf($, layout)).filter((r) => r.initiative_id === id && !accepted.has(r.run_id))
  const run = runs[runs.length - 1]
  return run ? { run_id: run.run_id, initiative_id: run.initiative_id, message: message.slice(0, 300) } : null
}

// What the session can see, gathered through `$`. Kept out of the pure modules on purpose.
async function gather($: any, repaired: string[] = [], repairError: string | null = null): Promise<CockpitStatus> {
  const root: string = await $.session.root()
  const layout = await detectLayout({ exists: (p: string) => $.fs.exists(p) }, root)
  const profileText: string | null = (await $.fs.exists(`${root}/.pmos-profile`))
    ? await $.fs.read(`${root}/.pmos-profile`).catch(() => null)
    : null
  const profile = parseProfile(profileText)

  let skills: SkillsReport | null = null
  if (layout.kind === 'kit' && layout.skillsNative && (await $.fs.exists(layout.skillsNative))) {
    const entries = await $.fs.list(layout.skillsNative)
    const native = []
    for (const e of entries) {
      if (e.kind !== 'dir') continue
      const text = await $.fs.read(`${layout.skillsNative}/${e.name}/SKILL.md`).catch(() => '')
      const real = await $.fs.stat(`${layout.skillsNative}/${e.name}`, { resolve: true }).catch(() => undefined)
      native.push({ name: e.name, descriptionOk: parseDescription(text).ok, realPath: real?.realPath ?? null })
    }
    const linked = new Map()
    if (await $.fs.exists(layout.claudeSkills)) {
      for (const e of await $.fs.list(layout.claudeSkills)) {
        const path = `${layout.claudeSkills}/${e.name}`
        const st = await $.fs.stat(path, { resolve: true }).catch(() => undefined)
        let target: string | null = null
        if (e.isLink) {
          const r = await $.process.run(['readlink', path]).catch(() => undefined)
          target = r && r.exitCode === 0 ? r.stdout.trim() : null
        }
        linked.set(e.name, {
          kind: st?.kind ?? e.kind,
          isLink: Boolean(e.isLink),
          realPath: st?.realPath ?? null,
          target,
        })
      }
    }
    skills = diffSkills(native, linked)
  }
  const facts = layout.kind === 'none' ? { branch: null, defaultBranch: null, commitsAhead: null } : await gitFacts($, root)
  const idGuess = facts.branch?.startsWith('initiative/') ? facts.branch.slice('initiative/'.length) : null
  const rp = idGuess ? recordPath({ ...layout, root }, idGuess) : null
  const initiative = resolveInitiative({
    branch: facts.branch,
    recordExists: rp ? await $.fs.exists(rp).catch(() => null) : null,
    commitsAhead: facts.commitsAhead,
  })
  return {
    layoutKind: layout.kind,
    branch: facts.branch,
    initiative,
    stateRoot: layout.stateRoot,
    profile: profile.profile,
    profileDeclared: profile.declared,
    profileNote: profile.note,
    skills,
    repaired,
    repairError,
  }
}

// The one write this module makes: a symbolic link under .claude/skills for each skill the kit ships
// and the repository does not register. A name that is not a slug is never used as a path.
async function repair($: any): Promise<CockpitStatus> {
  const before = await gather($)
  const todo = before.skills?.repairable ?? []
  const root: string = await $.session.root()
  const done: string[] = []
  let error: string | null = null
  if (todo.length > 0) {
    const dir = `${root}/.claude/skills`
    const mk = await $.process.run(['mkdir', '-p', dir])
    if (mk.exitCode !== 0) error = `could not create ${dir}: ${mk.stderr.trim()}`
    else {
      for (const name of todo) {
        if (!NAME_RE.test(name)) continue
        const path = `${dir}/${name}`
        // a dangling link the kit made is replaced; nothing else at that name ever is
        if ((await $.fs.stat(path).catch(() => undefined)) === undefined && (await $.fs.list(dir)).some((e: any) => e.name === name && e.isLink)) {
          await $.process.run(['rm', '-f', path])
        }
        const r = await $.process.run(['ln', '-s', linkTarget(name), path])
        if (r.exitCode === 0) done.push(name)
        else { error = `ln failed for ${name}: ${r.stderr.trim()}`; break }
      }
    }
  }
  return gather($, done, error)
}

// ── continuity (M13) ─────────────────────────────────────────────────────────────────────────────
// What the checkout looks like to a handover: git, the initiative the branch names, and what is changed
// or unpushed. Read-only; every process has the 5 second limit.
async function readCheckout($: any, root: string, layout: any) {
  const run = (argv: string[], limit = PROCESS_LIMIT_MS) => sh($, argv, { cwd: root, timeoutMs: limit })
  const r: Record<string, any> = {}
  for (const k of ['inside', 'branch', 'sha', 'dirs', 'defaultBranch'] as const) r[k] = await run(PROBES[k])
  const g = interpretGit(r)
  const idGuess = g.branch?.startsWith('initiative/') ? g.branch.slice('initiative/'.length) : null
  const rp = idGuess ? recordPath(layout, idGuess) : null
  const initiative = resolveInitiative({ branch: g.noGit ? null : g.branch, recordExists: rp ? await $.fs.exists(rp) : null, commitsAhead: null })
  let porcelain = ''
  let hasOrigin = false
  let unpushed = { unpushed: false }
  if (!g.noGit) {
    const st = await run(HANDOVER_PROBES.status as string[])
    porcelain = st.exitCode === 0 ? st.stdout : ''
    hasOrigin = (await run(HANDOVER_PROBES.origin as string[])).exitCode === 0
    const u: Record<string, any> = {}
    for (const [k, argv] of Object.entries(UNPUSHED_PROBES)) u[k] = await run(argv as string[])
    unpushed = interpretUnpushed(u)
  }
  return { run, g, initiative, porcelain, hasOrigin, unpushed }
}

// What the plan, and the confirmation that follows it, compare: the facts a plan was made for.
const keyOf = (c: any, words: string) => ({ branch: c.g.branch, sha: c.g.sha, porcelain: c.porcelain, words })

async function planHandover($: any, words: string): Promise<string> {
  const { root, layout } = await rootAndLayout($)
  const c = await readCheckout($, root, layout)
  const p = buildPlan({ layout, root, initiative: c.initiative, git: c.g, porcelain: c.porcelain, hasOrigin: c.hasOrigin, unpushed: c.unpushed, words, nowMs: await $.clock.now() })
  if (!p.ok) { await update($, handoverPlan, () => null); return p.text }
  await update($, handoverPlan, () => p.plan)
  return p.text
}

// Called only from a person's typed command or press. The plan must still match the checkout.
async function confirmHandover($: any, push: boolean): Promise<string> {
  const plan: any = await read($, handoverPlan)
  const { root, layout } = await rootAndLayout($)
  const c = await readCheckout($, root, layout)
  const now: number = await $.clock.now()
  const good = planStillGood(plan, plan ? keyOf(c, plan.words) : { branch: null, sha: null, porcelain: '', words: '' }, now)
  if (!good.ok) {
    await update($, handoverPlan, () => null)
    return `Nothing was written. ${good.why}${plan ? ' Run /pmos-handover again to see a new plan.' : ''}`
  }
  // the note's facts, read fresh: the ladder for this initiative, and how far the branch is from the default
  await refreshView($, true)
  const v: any = await read($, view)
  const def: string = c.g.defaultBranch ?? 'main'
  const ahead = await c.run(['git', 'rev-list', '--count', `${def}..HEAD`])
  const subj = await c.run(subjectsProbe(def) as string[])
  const noteText = renderNote({
    id: plan.id, branch: plan.branch, sha: c.g.sha, summary: v?.initiative?.id === plan.id ? v.summary : null,
    commits: ahead.exitCode === 0 && /^\d{1,9}$/.test(out(ahead)) ? Number(out(ahead)) : 0, defaultBranch: def,
    subjects: subj.exitCode === 0 ? out(subj).split('\n').filter(Boolean) : [], paths: plan.paths, words: plan.words, atIso: new Date(now).toISOString(),
  })
  // a note path that is a link, or that sits behind one, is never written through
  const rootReal = (await $.fs.stat(root, { resolve: true }).catch(() => undefined))?.realPath ?? root
  const noteDir = plan.noteRel.split('/').slice(0, -1).join('/')
  const write = async (path: string, text: string) => {
    for (const [p, expect] of [[path, `${rootReal}/${plan.noteRel}`], [`${root}/${noteDir}`, `${rootReal}/${noteDir}`]] as const) {
      const st = await $.fs.stat(p, { resolve: true }).catch(() => undefined)
      if (st && (st.isLink || (st.realPath && st.realPath !== expect))) throw new Error(`${p} is, or sits behind, a link`)
    }
    await $.fs.write(path, text)
  }
  const r = await carryOut({ plan, push, noteText, run: c.run, write })
  await update($, handoverPlan, () => null)
  await update($, resume, () => null)
  await refreshView($, true)
  return r.text
}

async function pressHandover($: any, push: boolean) {
  try { $.ui.toast(`pmos-cockpit: ${await confirmHandover($, push)}`) } catch (e: any) { $.ui.toast(`pmos-cockpit: the handover did not run. ${String(e?.message ?? e)}`) }
}

// At session start: the newest note for this branch's initiative, and how far the branch has moved.
async function loadResume($: any): Promise<void> {
  try {
    const { root, layout } = await rootAndLayout($)
    const v: any = await read($, view)
    const id: string | null = v?.initiative?.state === 'ok' ? v.initiative.id : null
    const np = id === null ? null : notePaths(layout, root, id)
    const text = np === null ? null : await readText($, np.abs)
    const note = text === null ? null : parseNote(text)
    if (id === null || np === null || note === null || note.id !== id || note.headSha === null) { await update($, resume, () => null); return }
    const run = (argv: string[]) => sh($, argv, { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
    const pr = sinceProbes(note, np.rel) as any
    const r: Record<string, any> = { ancestor: await run(pr.ancestor) }
    if (r.ancestor.exitCode === 0) {
      r.handoverCommit = await run(pr.handoverCommit)
      const h = out(r.handoverCommit)
      r.count = await run(countSince(/^[0-9a-f]{40,64}$/.test(h) ? h : note.headSha) as string[])
    }
    const line = resumeText(note, id, interpretSince(r))
    await update($, resume, () => (line === null ? null : { text: line }))
  } catch {
    await update($, resume, () => null)
  }
}

// Recording a reviewer's verdict: called only from a person's press or typed command. The offer is
// cleared before the writer runs, so a second press records nothing.
async function recordReview($: any): Promise<{ ok: boolean; text: string }> {
  const o: any = await read($, reviewOffer)
  if (!o) return { ok: false, text: 'There is no reviewer verdict waiting to be recorded.' }
  await update($, reviewOffer, () => null)
  const { root, layout } = await rootAndLayout($)
  const plan = planEval({ layout, root, review: o, atIso: new Date(await $.clock.now()).toISOString() })
  if (!plan.ok) return { ok: false, text: plan.reason }
  const r = await sh($, plan.argv, { cwd: root, env: plan.env, timeoutMs: PROCESS_LIMIT_MS })
  if (r.exitCode !== 0) return { ok: false, text: `Not recorded. ${(r.stderr || out(r)).trim()}` }
  return { ok: true, text: `Recorded ${o.verdict} ${o.score} for ${o.run_id}, evaluator ${o.evaluator_model}.${plan.storesFindings ? '' : ' The findings are not stored in a kit: copy them from above if you want them kept.'}` }
}

// The reviewer's answer: parsed, shown, and held. This writes nothing.
async function takeReviewerAnswer($: any, e: any): Promise<void> {
  try {
    const pending: any = await read($, review)
    if (!pending || e.agentId !== pending.agentId) return
    await update($, review, () => null)
    const v = parseVerdict(e.answer)
    if (v.ok) {
      const o = { run_id: pending.run_id, initiative_id: pending.initiative_id, rubric_path: pending.rubric_path, verdict: v.verdict, score: v.score, evaluator_model: pending.model, findings: v.findings }
      await update($, reviewOffer, () => o)
      $.ui.toast(`pmos-cockpit: ${offerText(o)} Press Record above the prompt, or type /pmos-record-review.`)
    } else {
      $.ui.toast(`pmos-cockpit: the reviewer's answer had no verdict, so nothing is offered. ${v.reason}.`)
    }
  } catch { /* bookkeeping for a convenience: never reaches the turn */ }
}

const baseName = (p: string): string => p.split('/').filter(Boolean).pop() ?? p

// The session view (M11): one read of the checkout and, at most once a minute for each commit, one read
// of the ladder for the one initiative the branch names. Read-only, bounded, and never in the way: a
// failure leaves the last view as it was, and nothing here reaches the turn that called it.
async function refreshView($: any, force = false): Promise<void> {
  try {
    const started: number = await $.clock.now()
    const { root, layout } = await rootAndLayout($)
    if (layout.kind === 'none') { $.ui.status(undefined); await update($, view, () => null); return }
    // every process has its own limit, and the whole refresh has one: once it is spent, nothing more runs
    const run = async (argv: string[]) =>
      (await $.clock.now()) - started > PROCESS_LIMIT_MS ? { exitCode: 1, stdout: '', stderr: 'the view ran out of time' } : sh($, argv, { cwd: root, timeoutMs: PROCESS_LIMIT_MS })

    const r: Record<string, any> = {}
    for (const k of ['inside', 'branch', 'sha', 'dirs', 'defaultBranch'] as const) r[k] = await run(PROBES[k])
    let g = interpretGit(r)
    if (!g.noGit && g.branch && !g.branch.startsWith('initiative/')) { r.ahead = await run(aheadProbe(g.defaultBranch)); g = interpretGit(r) }

    const idGuess = g.branch?.startsWith('initiative/') ? g.branch.slice('initiative/'.length) : null
    const rp = idGuess ? recordPath(layout, idGuess) : null
    const initiative = resolveInitiative({
      branch: g.noGit ? null : g.branch,
      recordExists: rp ? await $.fs.exists(rp) : null,
      commitsAhead: g.commitsAhead,
    })

    let summary: any = null
    let unavailable: string | null = null
    let links: { record: string | null; prd: string | null; rubric: string | null } | null = null
    if (!g.noGit && initiative.state === 'ok' && initiative.id) {
      const now: number = await $.clock.now()
      const sha = g.sha ?? 'none'
      const cached: any = await read($, factsCache)
      if (!force && isFresh(cached, initiative.id, sha, now)) summary = cached.summary
      else {
        const ws = layout.kind === 'kit' ? `${root}/pmos/scripts/workflow-state.js` : `${root}/scripts/workflow-state.js`
        if (!(await $.fs.exists(ws))) unavailable = 'workflow-state.js is not installed here'
        else {
          const p = parseFactsOutput(await run(['node', ws, initiative.id, '--json']))
          if (p.ok) {
            summary = summarize(p.json)
            await update($, factsCache, () => ({ id: initiative.id, sha, at: now, summary }))
          } else unavailable = p.reason
        }
      }
      // the record's own paths, for the slice: shown to the person, never given to the model
      const text = rp ? await readText($, rp) : null
      const field = (name: string): string | null => {
        const m = text === null ? null : new RegExp(`^${name}:[ \\t]*(\\S[^\\n]*)$`, 'm').exec(text)
        return m ? clean(m[1].replace(/^["']|["']$/g, ''), 80) : null
      }
      links = { record: rp ? clean(rp.replace(`${root}/`, ''), 80) : null, prd: field('prd_path'), rubric: field('rubric_path') }
    }

    // cloud only: what would be lost with the session. Read-only git; the band says it, a person acts.
    let cloud: string | null = null
    if (((await read($, modeAtom)) as any)?.mode === 'cloud' && !g.noGit) {
      const u: Record<string, any> = {}
      for (const [k, argv] of Object.entries(UNPUSHED_PROBES)) u[k] = await run(argv as string[])
      cloud = cloudBandText(interpretUnpushed(u), g.branch)
    }

    const text = formatStatus({
      repo: baseName(root),
      worktree: g.linked ? baseName(root) : null,
      branch: g.branch,
      noGit: g.noGit,
      initiative,
      facts: summary,
      width: lastColumns > 0 ? lastColumns : undefined,
    })
    $.ui.status(text)
    const stateRel = layout.stateRoot ? layout.stateRoot.replace(`${root}/`, '') : null
    await update($, view, () => ({ text, initiative, summary, unavailable, links, sha: g.sha, at: started, stateRel, noGit: g.noGit, cloud }))
  } catch {
    /* the view is a convenience: on any failure the last view stays, and the turn is not told */
  }
}

const summary = (s: CockpitStatus): string => {
  const k = s.skills
  const lines = [
    `layout   ${s.layoutKind}${s.stateRoot ? ` (state: ${s.stateRoot})` : ''}`,
    `profile  ${s.profile}${s.profileDeclared ? '' : ` (${s.profileNote ?? 'not declared'}; treated as product-code)`}`,
    `initiative  ${s.initiative.text}`,
  ]
  if (s.layoutKind !== 'kit') lines.push('skills   registration is checked in a vendored kit only')
  else if (k) {
    lines.push(`skills   ${k.ok.length} registered, ${k.missing.length} missing${k.repairable.length ? ` (${k.repairable.length} repairable)` : ''}`)
    if (k.blocked.length) lines.push(`blocked  ${k.blocked.map((b) => `${b.name} (${b.reason})`).join('; ')}`)
    if (k.badDescription.length) lines.push(`no description  ${k.badDescription.join(', ')} (not linked)`)
  } else lines.push('skills   no pmos/skills-native folder found')
  if (s.repaired.length) lines.push(`repaired ${s.repaired.join(', ')}. Claude Code reads new links in the next session.`)
  if (s.repairError) lines.push(`error    ${s.repairError}`)
  return lines.join('\n')
}

const hasDrift = (s: CockpitStatus | null): boolean =>
  s?.skills != null && (s.skills.missing.length > 0 || s.skills.blocked.length > 0 || s.skills.badDescription.length > 0)

async function pressAccept($: any, label: 'pass' | 'fail') {
  const o: any = await read($, offer)
  if (!o) return
  const r = await writeAcceptance($, o, label, pressNote(o.message, label, o.run_id))
  await update($, offer, () => null)
  $.ui.toast(r.ok ? `pmos-cockpit: recorded ${label} for ${o.run_id}.` : `pmos-cockpit: could not record it. ${r.text}`)
}

export const register: Register = (on) => {
  on('session.start', async ($, e, next) => {
    await $.command.register({ name: 'pmos-status', description: 'Show the PMOS layout, profile and skill registration' })
    await $.command.register({ name: 'pmos-repair-skills', description: 'Link the kit skills that are not registered in .claude/skills' })
    await $.command.register({ name: 'pmos-grant', description: 'Mint one single-use grant: /pmos-grant merge:<PR> | publish:<artifact> [minutes] [initiative:<id>] [your words]' })
    await $.command.register({ name: 'pmos-slice', description: 'Open the initiative slice: the ladder, step by step, for the branch you are on' })
    await $.command.register({ name: 'pmos-handover', description: 'Hand the branch over: /pmos-handover [your words] shows a plan; /pmos-handover confirm [local] commits and pushes it' })
    await $.command.register({ name: 'pmos-accept', description: 'Record your Acceptance of a run: /pmos-accept <run_id> pass|fail <note>' })
    await $.command.register({ name: 'pmos-review', description: 'Ask a read-only reviewer on another model to judge the newest run: /pmos-review [model]' })
    await $.command.register({ name: 'pmos-record-review', description: 'Record the reviewer verdict that is waiting (the same as pressing Record)' })
    await $.command.register({ name: 'pmos-route', description: 'Model routing for this session: /pmos-route on | off | status' })
    try {
      await $.agent.register({ name: REVIEWER_AGENT, description: 'Independent read-only reviewer started by /pmos-review. Not for the builder to call.', prompt: REVIEWER_PROMPT, tools: [...REVIEWER_TOOLS] })
    } catch { /* the review command says so when it cannot start the reviewer */ }
    const m = await readMode($)
    await update($, modeAtom, () => m)
    const s = await gather($)
    await update($, status, () => s)
    await refreshView($)
    await update($, resumeGone, () => false)
    await loadResume($)
    if (s.skills && s.skills.missing.length > 0) {
      $.ui.toast(`PMOS: ${s.skills.missing.length} kit skill(s) are not registered. Run /pmos-repair-skills.`)
    }
    return next(e)
  })

  on('command.run', { command: 'pmos-status' }, async ($) => {
    const s = await gather($)
    await update($, status, () => s)
    await refreshView($, true)
    const v: any = await read($, view)
    const lines = [summary(s)]
    const m: any = await read($, modeAtom)
    if (m) lines.push(describeMode(m))
    const rs: any = await read($, resume)
    if (rs?.text) lines.push(`resume   ${String(rs.text).replace(/^PMOS: /, '')}`)
    try { lines.push(`surfaces ${(await $.session.surfaces()).join(', ') || 'none reported'}`) } catch { lines.push('surfaces unavailable') }
    if (v?.summary) lines.push(`stage    ${v.summary.stage}${v.summary.next ? ` (next: ${v.summary.next.key})` : ''}${v.summary.gateFailures.length ? `; blocking: ${v.summary.gateFailures.join(', ')}` : ''}`)
    else if (v?.unavailable) lines.push(`stage    unavailable: ${v.unavailable}`)
    return { text: lines.join('\n') }
  })

  // The slice opens only when asked, and not at all when the workspace already shows it (hosted).
  on('command.run', { command: 'pmos-slice' }, async ($) => {
    const m: any = await read($, modeAtom)
    const policy = slicePolicy(m?.mode ?? 'local')
    if (!policy.opens) return { text: policy.text }
    await refreshView($)
    await $.ui.open({ id: 'pmos-slice', title: 'PMOS initiative' })
    return { text: policy.text }
  })

  on('ui.render', { component: 'Pane', requestId: 'pmos-slice' }, async ($, e) => {
    const { Box, Text } = $.ui.resolve(e)
    const v: any = await read($, view)
    if (!v || !v.initiative || v.initiative.id === null) {
      return (
        <Box>
          <Text>{clean(v?.noGit ? 'No git repository here.' : v?.initiative?.text ?? 'No PMOS state here.', 120)}</Text>
        </Box>
      )
    }
    const room = Math.max(4, (e.viewport?.rows ?? 24) - 8)
    const rows = v.summary ? sliceRows(v.summary).slice(0, room) : []
    const when = new Date(v.at).toISOString().slice(11, 19)
    return (
      <Box>
        <Text>
          {v.initiative.id} · {v.summary ? `${v.summary.lane} · ${v.summary.stage}` : 'stage unavailable'}
        </Text>
        {v.unavailable && <Text>{clean(v.unavailable, 80)}</Text>}
        {rows.map((r: any) => (
          <Text key={r.key}>
            {r.glyph} {r.label}
          </Text>
        ))}
        {v.links?.record && <Text>record {v.links.record}</Text>}
        {v.links?.prd && <Text>prd {v.links.prd}</Text>}
        {v.links?.rubric && <Text>rubric {v.links.rubric}</Text>}
        <Text>
          derived {v.sha ? String(v.sha).slice(0, 7) : 'no commit'} at {when}Z
        </Text>
      </Box>
    )
  })

  // The status line follows the branch: refreshed at the end of each turn, never per tool call.
  on('turn.complete', async ($, e, next) => {
    await takeReviewerAnswer($, e)
    await refreshView($)
    return next(e)
  })

  // The one thing this mod says to the model on its own. A fixed template over allow-listed fields,
  // read from the last view, so composing the prompt runs no process.
  on('prompt.compose', async ($, e, next) => {
    const r = await next(e)
    try {
      const v: any = await read($, view)
      if (!v?.initiative?.id) return r
      const text = buildSection({
        id: v.initiative.id,
        lane: v.summary?.lane,
        anchor: v.summary?.anchor,
        stage: v.summary?.stage,
        nextKey: v.summary?.next?.key ?? null,
        stateRoot: v.stateRel,
        profile: v.summary?.profile,
      })
      return text === null ? r : { ...r, sections: [...r.sections, { id: SECTION_ID, text, scope: 'session' as const }] }
    } catch {
      return r
    }
  })

  on('command.run', { command: 'pmos-repair-skills' }, async ($) => {
    const s = await repair($)
    await update($, status, () => s)
    return { text: summary(s) }
  })


  // ── the guards ──────────────────────────────────────────────────────────────────────────────
  // Every tool call passes through here. Reading, editing and testing go straight on. If the guard
  // itself fails, names alone decide: a merge, a publish and a write to a ledger or governing file
  // are denied, and an ordinary call is allowed (M17).
  on('tool.call', async ($, e, next) => {
    let verdict: string | null
    try {
      verdict = await judge($, e)
    } catch {
      verdict = looseDeny(e)
    }
    if (verdict !== null) return { deny: verdict }
    return next(e)
  })

  // An outward write asks, each time. The answer is a person's press and is stored nowhere.
  on('tool.check', async ($, e, next) => {
    const v = await next(e)
    if (v.decision !== 'allow') return v
    try {
      const f = classifyTool({ tool: e.tool, ...((e.input as object) ?? {}) }, { root: '', layout: null })
      if (f && f.kind === 'outward') {
        return { decision: 'ask', reason: `pmos-cockpit: ${e.tool} writes outside this repository. Confirm this item; a confirmation is not stored as a grant.` }
      }
    } catch { /* not an outward write that could be read: leave the engine's verdict */ }
    return v
  })

  // ── the independent evaluator (M14) ─────────────────────────────────────────────────────────
  // The builder's model never sees the reviewer in its list of agents: only the person starts it.
  on('agent.offer', { agent: REVIEWER_TYPE }, () => ({ isOffered: false }))

  on('command.run', { command: 'pmos-review' }, async ($, e) => {
    if (!isPerson(e)) return { text: 'A review is started only by your own typed command. This run did not come from you, so nothing was started.' }
    if (await read($, review)) return { text: 'A review is already running. Its verdict will appear above the prompt.' }
    try {
      const { root, layout } = await rootAndLayout($)
      if (layout.kind === 'none') return { text: 'No review started. There is no PMOS state here.' }
      const facts = await gitFacts($, root)
      const id = facts.branch?.startsWith('initiative/') ? facts.branch.slice('initiative/'.length) : null
      if (id === null) return { text: 'No review started. The branch names no initiative.' }
      const run = (await runsOf($, layout)).filter((r) => r.initiative_id === id).pop()
      if (!run) return { text: `No review started. Initiative ${id} has no run in ${layout.stateRoot ?? 'the state root'}.` }
      const rp = recordPath(layout, id)
      const rec = rp ? await readText($, rp) : null
      const m = rec === null ? null : /^rubric_path:[ \t]*(\S[^\n]*)$/m.exec(rec)
      const rubricPath = m ? m[1].replace(/^["']|["']$/g, '').trim() : ''
      const model = pickReviewerModel(await $.session.model(), String(e.args ?? '').trim())
      if (!model.ok) return { text: `No review started. ${model.reason}` }
      const prompt = buildReviewPrompt({ initiativeId: id, runId: run.run_id, rubricPath })
      if (!prompt.ok) return { text: `No review started. ${prompt.reason}.` }
      const sp: any = await $.agent.spawn({ subagentType: REVIEWER_TYPE, prompt: prompt.text, description: `review ${run.run_id}`, model: model.model })
      if (sp?.deny || typeof sp?.agentId !== 'string') return { text: `No review started. ${sp?.deny ?? 'the reviewer could not be started.'}` }
      await update($, review, () => ({ agentId: sp.agentId, run_id: run.run_id, initiative_id: id, rubric_path: rubricPath, model: model.model }))
      return { text: `Started a read-only reviewer on ${model.model} (builder family ${model.builderFamily}) for run ${run.run_id}. Its verdict will appear above the prompt, and nothing is recorded until you press Record.` }
    } catch (err: any) {
      return { text: `No review started. ${String(err?.message ?? err).slice(0, 200)}` }
    }
  })

  on('command.run', { command: 'pmos-record-review' }, async ($, e) => {
    if (!isPerson(e)) return { text: 'A verdict is recorded only by your own typed command or press. This run did not come from you, so nothing was written.' }
    return { text: (await recordReview($)).text }
  })

  // ── commands that are a person's act ────────────────────────────────────────────────────────
  on('command.run', { command: 'pmos-grant' }, async ($, e) => {
    if (!isPerson(e)) return { text: 'A grant is minted only by your own typed command or press. This run did not come from you, so nothing was written.' }
    const { root, layout } = await rootAndLayout($)
    // the branch's initiative is recorded on the grant by default, when its record exists
    const facts = await gitFacts($, root)
    const idGuess = facts.branch?.startsWith('initiative/') ? facts.branch.slice('initiative/'.length) : null
    const rp = idGuess ? recordPath(layout, idGuess) : null
    const bi = resolveInitiative({ branch: facts.branch, recordExists: rp ? await $.fs.exists(rp) : null, commitsAhead: null })
    const plan = planGrant(e.args, { layout, root, pm: await whoAmI($, root), branchInitiative: bi.state === 'ok' ? bi.id : null })
    if (!plan.ok) return { text: plan.text }
    if (plan.initiative !== null) {
      const rec = recordPath(layout, plan.initiative)
      if (rec === null || !(await $.fs.exists(rec))) return { text: `No grant written. Initiative ${plan.initiative} has no record${rec ? ` at ${rec.replace(`${root}/`, '')}` : ''}.` }
    }
    const r = await sh($, plan.argv, { cwd: root, env: plan.env })
    return { text: r.exitCode === 0 ? `Granted ${plan.scope.kind}:${plan.scope.target}, single use, ${plan.ttl} minutes. ${plan.note} ${out(r).split('\n').pop()}` : `No grant written. ${(r.stderr || out(r)).trim()}` }
  })

  // A handover is a person's act: a plan first, and only a typed confirm or a press carries it out.
  on('command.run', { command: 'pmos-handover' }, async ($, e) => {
    if (!isPerson(e)) return { text: 'A handover commits and pushes only on your own typed command or press. This run did not come from you, so nothing was done.' }
    const args = String(e.args ?? '').trim()
    const m = /^confirm(?:[ \t]+(local))?$/.exec(args)
    try {
      return { text: m ? await confirmHandover($, m[1] !== 'local') : await planHandover($, args) }
    } catch (err: any) {
      return { text: `Nothing was done. The handover could not run: ${String(err?.message ?? err)}` }
    }
  })

  on('command.run', { command: 'pmos-accept' }, async ($, e) => {
    if (!isPerson(e)) return { text: 'An acceptance is recorded only by your own typed command or press. This run did not come from you, so nothing was written.' }
    const a = parseAccept(e.args)
    if (!a.ok) return { text: a.text }
    const { layout } = await rootAndLayout($)
    const run = (await runsOf($, layout)).find((r) => r.run_id === a.run_id)
    if (!run) return { text: `No acceptance written. Run ${a.run_id} is not in ${layout.stateRoot ?? 'the state root'}.` }
    const r = await writeAcceptance($, run, a.label as 'pass' | 'fail', a.note)
    return { text: r.ok ? `Recorded ${a.label} for ${a.run_id}.` : `No acceptance written. ${r.text}` }
  })

  // A message that reads as approval gets a button. Nothing is written until a person presses it.
  on('prompt.submit', async ($, e, next) => {
    if (e.origin?.kind === 'composer' && readsAsApproval(e.text)) {
      try { const o = await buildOffer($, e.text); await update($, offer, () => o) } catch { /* no offer */ }
    }
    // Model routing. A prompt that needs another model enters nothing now: the mod runs `/model` and
    // then submits the same text as the person's own, so the turn runs on the routed model. Anything
    // that fails on the way sends the prompt on unchanged; a prompt is never lost or held back.
    const r: any = await read($, routing)
    if (!r?.on || !shouldRoute(e).ok) return next(e)
    const answer = await askRoute($, e.text, 'pm')
    let current: string | undefined
    try { current = await $.session.model() } catch { current = undefined }
    const d: any = decide(answer, current as any)
    if (d.action !== 'switch') {
      if (d.tell && !r.told) {
        await update($, routing, (x: any) => ({ ...x, told: true }))
        $.ui.toast(`PMOS did not switch the model: ${d.why}.`.slice(0, 280))
      }
      return next(e)
    }
    const text = e.text
    $.clock.after(0, () => {
      void (async () => {
        try { await $.command.run({ command: 'model', args: d.model }) } catch { /* the prompt still goes, on the current model */ }
        try { await $.prompt.submit({ text, asUser: true }) } catch { $.ui.toast('PMOS could not send your prompt after switching the model. Send it again.') }
        $.ui.toast(switchNote(d))
      })()
    })
    return { text }
  })

  // Every prompt handed to a sub-agent is routed, unless the call named a model or the agent is pinned.
  on('agent.spawn', async ($, e, next) => {
    const r: any = await read($, routing)
    if (!r?.on || e.model || e.subagentType === REVIEWER_TYPE || e.subagentType === 'fork' || typeof e.prompt !== 'string' || !e.prompt.trim()) return next(e)
    const model = subagentModel(e, await askRoute($, e.prompt, 'agent'))
    return next(model ? { ...e, model } : e)
  })

  on('command.run', { command: 'pmos-route' }, async ($, e) => {
    const arg = String(e.args ?? '').trim()
    if (arg === 'on' || arg === 'off') {
      if (arg === 'on' && !isPerson(e)) return { text: 'Routing is turned on only by your own typed command. Nothing changed.' }
      await update($, routing, (x: any) => ({ ...x, on: arg === 'on' }))
      return { text: arg === 'on' ? 'Model routing is on for this session: each prompt you type and each sub-agent prompt is routed before it runs.' : 'Model routing is off for this session. The model changes only when you change it.' }
    }
    const r: any = await read($, routing)
    let url: string | null = null
    try { url = routeUrl(await $.env.get('PMOS_ROUTE_URL')) } catch { url = null }
    return { text: `Model routing is ${r?.on ? 'on' : 'off'} for this session. ${url ? 'The workspace answers routes; the model switches only when Jev or a rule decided.' : 'This terminal was not started by the PMOS workspace, so no prompt is routed.'}` }
  })

  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
    if (typeof e.viewport?.columns === 'number') lastColumns = e.viewport.columns
    const s = await read($, status)
    const o: any = await read($, offer)
    if (o && !e.props.hasSurvey) {
      const { Box, Button, Text } = $.ui.resolve(e)
      return (
        <Box>
          <Text>PMOS: record your acceptance of run {o.run_id}? </Text>
          <Button key="pass" label="Pass" variant="primary" onPress={() => pressAccept($, 'pass')} />
          <Button key="fail" label="Fail" onPress={() => pressAccept($, 'fail')} />
          <Button key="later" label="Not now" onPress={() => update($, offer, () => null)} />
        </Box>
      )
    }
    const hp: any = await read($, handoverPlan)
    if (hp && !e.props.hasSurvey) {
      const { Box, Button, Text } = $.ui.resolve(e)
      return (
        <Box>
          <Text>PMOS: handover plan for {hp.id}, {hp.paths.length} path(s){hp.push.run ? `, then ${hp.push.text}` : ', no push'}. </Text>
          {hp.push.run && <Button key="go" label="Commit and push" variant="primary" onPress={() => pressHandover($, true)} />}
          <Button key="local" label="Commit only" onPress={() => pressHandover($, false)} />
          <Button key="cancel" label="Cancel" onPress={() => update($, handoverPlan, () => null)} />
        </Box>
      )
    }
    const rv: any = await read($, reviewOffer)
    if (rv && !e.props.hasSurvey) {
      const { Box, Button, Text } = $.ui.resolve(e)
      return (
        <Box>
          <Text>PMOS: {offerText(rv)} </Text>
          <Button key="record" label="Record" variant="primary" onPress={async () => { const r = await recordReview($); $.ui.toast(`pmos-cockpit: ${r.text}`) }} />
          <Button key="later" label="Not now" onPress={() => update($, reviewOffer, () => null)} />
        </Box>
      )
    }
    if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
    // one line, only when something blocks, in this order: skill drift, a blocking ladder step, a branch
    // with commits and no initiative record. The text comes from checked fields, never from a record.
    const v: any = await read($, view)
    if (!hasDrift(s) && v?.initiative?.id && v?.summary?.gateFailures?.length) {
      const { Box, Button, Text } = $.ui.resolve(e)
      return (
        <Box>
          <Text>
            PMOS: {v.initiative.id} has a blocking order violation ({v.summary.gateFailures.join(', ')}).{v.summary.next ? ` Next: ${v.summary.next.key}.` : ''}{' '}
          </Text>
          <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
        </Box>
      )
    }
    if (!hasDrift(s) && v?.initiative?.warn) {
      const { Box, Button, Text } = $.ui.resolve(e)
      return (
        <Box>
          <Text>PMOS: {clean(v.initiative.text, 120)}. </Text>
          <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
        </Box>
      )
    }
    if (!hasDrift(s) && v?.cloud) {
      const { Box, Button, Text } = $.ui.resolve(e)
      return (
        <Box>
          <Text>{clean(v.cloud, 300)} </Text>
          <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
        </Box>
      )
    }
    const rs: any = await read($, resume)
    if (!hasDrift(s) && rs?.text && !(await read($, resumeGone))) {
      const { Box, Button, Text } = $.ui.resolve(e)
      return (
        <Box>
          <Text>{clean(rs.text, 400)} </Text>
          <Button key="dismiss" label="Dismiss" onPress={() => update($, resumeGone, () => true)} />
        </Box>
      )
    }
    if (!hasDrift(s)) return next(e)
    const { Box, Button, Text } = $.ui.resolve(e)
    const k = s!.skills!
    const parts = [`${k.missing.length} kit skill(s) are not registered`]
    if (k.blocked.length) parts.push(`${k.blocked.length} blocked by something already in .claude/skills`)
    if (k.badDescription.length) parts.push(`${k.badDescription.length} have no description`)
    return (
      <Box>
        <Text>PMOS: {parts.join(', ')}. </Text>
        {k.repairable.length > 0 && (
          <Button
            key="repair"
            label="Repair"
            onPress={async () => {
              const r = await repair($)
              await update($, status, () => r)
            }}
          />
        )}
        <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
      </Box>
    )
  })
}
