export const meta = {
  name: 'new-final-review',
  description:
    "Cross-batch final code review for /new. Fans out a multi-agent review (Codex primary + doc-reviewer + api-perf-cost-auditor + qa-sentinel gates) over the WHOLE batch diff. Each domain specialist OWNS its lane end-to-end — it FP-checks its own findings in the finding pass, so there is no generic code-reviewer re-judge over another specialist's domain (cross-domain) nor over its own findings (self-judge); any residual unresolved finding is routed to its domain specialist. Read-only: returns classified findings + a gate table, applies NO fixes (the calling skill owns fix application + user gates). Maps to references/final-review.md steps F.2–F.4.",
  phases: [
    { title: 'Baseline', detail: 'architecture grounding for the batch scope (F.2)' },
    { title: 'Review', detail: 'parallel multi-agent review of the batch diff (F.3)' },
    { title: 'Verify', detail: 'specialist-owned validation; residual routed to domain specialist (F.4)' },
  ],
}

// ───────────────────────────────────────────────────────────────────────────
// args contract — supplied by the /new skill at F.1 (it owns git + tracker):
//   firstCardId        string                first card id, for labels/ids
//   worktreePath       string                the batch worktree
//   baseBranch         string                trunk the diff is taken against
//   cardPaths          string[]              backlog YAML paths (agents Read them)
//   reviewScopeFiles   string[]              FULL union of touched files (F.1)
//   archBaselinePaths  string[] | null       per-card baselines; null → spawn architect (F.2)
//   hasApiDataFiles    boolean               gates api-perf-cost-auditor (default true)
//   config             object                resolved baldart.config.yml (paths.*, …)
//
// Return value (consumed by the skill at F.5):
//   { codexEngine, findings:[…classified, FALSE_POSITIVE dropped], gateTable, summary }
// ───────────────────────────────────────────────────────────────────────────

const a = args || {}
const scope = Array.isArray(a.reviewScopeFiles) ? a.reviewScopeFiles : []
const cards = Array.isArray(a.cardPaths) ? a.cardPaths : []
const cfg = a.config || {}
const highRisk = (cfg.paths && cfg.paths.high_risk_modules) || [] // security-domain hint
const protocolRef = '.claude/skills/new/references/final-review.md'

if (!scope.length) {
  log('new-final-review: empty review scope — nothing to review.')
  return { codexEngine: 'none', findings: [], gateTable: [], summary: emptySummary() }
}

// ---- Schemas ----------------------------------------------------------------
const FINDING = {
  type: 'object',
  required: ['finding_id', 'title', 'severity', 'confidence', 'evidence', 'minimal_fix_direction', 'domain'],
  additionalProperties: false,
  properties: {
    finding_id: { type: 'string', description: '<CARD-ID>-F### or <agent>-F###' },
    title: { type: 'string' },
    severity: { enum: ['BLOCKER', 'HIGH', 'MEDIUM', 'LOW'] },
    confidence: { type: 'number', description: '0-100' },
    evidence: { type: 'string', description: 'exact file:line + code quote' },
    minimal_fix_direction: { type: 'string' },
    domain: { enum: ['doc', 'security', 'migration', 'code', 'perf', 'test'], description: 'Domain-Override routing bucket' },
  },
}
const FINDINGS_SCHEMA = {
  type: 'object', required: ['findings'], additionalProperties: false,
  properties: { findings: { type: 'array', items: FINDING }, note: { type: 'string' } },
}
const CODEX_SCHEMA = {
  type: 'object', required: ['codexAvailable', 'findings'], additionalProperties: false,
  properties: {
    codexAvailable: { type: 'boolean', description: 'false if CODEX_NOT_FOUND / TIMED_OUT' },
    findings: { type: 'array', items: FINDING },
    note: { type: 'string' },
  },
}
const GATES_SCHEMA = {
  type: 'object', required: ['gates'], additionalProperties: false,
  properties: {
    gates: {
      type: 'array',
      items: {
        type: 'object', required: ['gate', 'status'], additionalProperties: false,
        properties: { gate: { type: 'string' }, status: { enum: ['PASS', 'FAIL', 'SKIP'] }, detail: { type: 'string' } },
      },
    },
  },
}
const VERDICT_SCHEMA = {
  type: 'object', required: ['classification'], additionalProperties: false,
  properties: {
    classification: { enum: ['VERIFIED', 'FALSE_POSITIVE', 'NEEDS_MANUAL_CONFIRMATION'] },
    rationale: { type: 'string' },
  },
}

// ---- Shared brief fragments -------------------------------------------------
const scopeBrief = [
  `Worktree: ${a.worktreePath || '(cwd)'}`,
  `Base branch for the diff: ${a.baseBranch || '(trunk)'}`,
  `Backlog cards (Read each for acceptance_criteria + entrypoints):\n${cards.join('\n') || '(none)'}`,
  `Files changed — review ALL of these in full:\n${scope.join('\n')}`,
].join('\n\n')

// ───────────────────────────────────────────────────────────────────────────
// Phase Baseline (F.2) — reuse per-card baselines when present, else architect ×1
// ───────────────────────────────────────────────────────────────────────────
phase('Baseline')
let baselineBrief
const baselinePaths = Array.isArray(a.archBaselinePaths) ? a.archBaselinePaths.filter(Boolean) : []
if (baselinePaths.length) {
  // Pass-by-path (context economy): agents Read these on demand.
  baselineBrief = `Architecture baseline files (Read each — file paths, type signatures, patterns, high-risk paths):\n${baselinePaths.join('\n')}`
  log(`Baseline: reusing ${baselinePaths.length} per-card baseline file(s).`)
} else {
  const arch = await agent(
    `You are grounding a post-implementation code review. Map the EXISTING architecture, critical patterns, and high-risk code paths relevant to this batch's changed files, so downstream reviewers can spot regressions.\n\n${scopeBrief}\n\nReturn a concise baseline (the key modules, their contracts, and the regression-prone seams touched by this diff). Do not review the changes yet.`,
    { label: 'arch-baseline', phase: 'Baseline', agentType: 'codebase-architect',
      schema: { type: 'object', required: ['baseline'], additionalProperties: false, properties: { baseline: { type: 'string' } } } }
  )
  baselineBrief = `Architecture baseline (from codebase-architect):\n${(arch && arch.baseline) || '(unavailable)'}`
  log('Baseline: spawned codebase-architect (no per-card baselines supplied).')
}

// ───────────────────────────────────────────────────────────────────────────
// Phase Review (F.3) — Codex primary ‖ doc-reviewer ‖ api-perf-cost-auditor ‖ qa-sentinel
// ───────────────────────────────────────────────────────────────────────────
phase('Review')

// F-040 — DETERMINISTIC Codex pre-flight (fixes the false-negative where Codex is
// installed+working but the review subagent reported codexAvailable:false because a
// synchronous inline run hit its Bash timeout). The workflow sandbox has no Bash, so a
// minimal Haiku agent runs ONLY the resolution glob and returns the path — the existence
// decision is thus taken OUT of the review agent's hands. Found → Codex runs in the
// BACKGROUND with file-poll (canonical final-review.md F.3/F.4). Not found → straight to
// the code-reviewer fallback, no wasted Codex agent.
const PREFLIGHT_SCHEMA = {
  type: 'object', required: ['codexScriptPath'], additionalProperties: false,
  properties: { codexScriptPath: { type: 'string', description: 'absolute path to codex-companion.mjs, or "" if none' } },
}
let codexScriptPath = ''
try {
  const pf = await agent(
    `Resolve the Codex companion script path. Run EXACTLY this one Bash command and nothing else:\n` +
      "  ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex/scripts/codex-companion.mjs ~/.claude/plugins/cache/openai-codex/codex/*/scripts/codex-companion.mjs 2>/dev/null | sort -V | tail -1\n" +
      `Return codexScriptPath = the trimmed stdout (a single absolute path), or "" if the command printed nothing. Do NOT run Codex, do NOT read any other file, do NOT reason about availability — just report the command's stdout.`,
    { label: 'codex-preflight', phase: 'Review', model: 'haiku', schema: PREFLIGHT_SCHEMA }
  )
  codexScriptPath = (pf && typeof pf.codexScriptPath === 'string') ? pf.codexScriptPath.trim() : ''
} catch (_) { codexScriptPath = '' }
const codexResolved = /codex-companion\.mjs$/.test(codexScriptPath)
log(codexResolved
  ? `Codex companion resolved deterministically: ${codexScriptPath}`
  : 'Codex companion NOT found (deterministic pre-flight) — primary review via code-reviewer fallback.')

const codexPrompt =
  `Run a deep, cross-model code review over this batch diff, following the /codexreview protocol summarized in ${protocolRef} (Step F.3). The code is already written and committed — find bugs, regressions, security issues, and quality problems.\n\n` +
  `The Codex companion script is ALREADY CONFIRMED PRESENT at:\n  ${codexScriptPath}\n` +
  `Launch it in the BACKGROUND and poll for completion — do NOT run it synchronously (a sync run would hit the Bash tool timeout and is the exact bug this design fixes):\n` +
  `  • REVIEW_FILE=a unique /tmp file (e.g. /tmp/codexreview-batch-${a.firstCardId || 'batch'}-$$.md)\n` +
  `  • node "${codexScriptPath}" task "<your review instructions>" > "$REVIEW_FILE" 2>&1   — run this with run_in_background:true (the launching call returns immediately).\n` +
  `  • Then POLL $REVIEW_FILE (BashOutput / repeated reads) until it holds a terminal result, up to a full 10-minute window.\n` +
  `Return codexAvailable:false ONLY if $REVIEW_FILE ends up containing "CODEX_NOT_FOUND" or stays empty after the FULL 10-minute window — NEVER because a single Bash call returned slowly. The script's existence is already verified, so a premature false here is a bug, not a capability gap.\n\n` +
  `${scopeBrief}\n\n${baselineBrief}\n\n` +
  `For each finding return: finding_id, title, severity (BLOCKER|HIGH|MEDIUM|LOW), confidence (0-100), evidence (exact file:line + code quote), minimal_fix_direction, and domain (doc|security|migration|code|perf|test). ` +
  `Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated). Set codexAvailable:true when the review ran.`

const docPrompt =
  `Cross-card documentation + SSOT-registry review over the batch diff, per ${protocolRef} Step F.3 (doc-reviewer row). Check doc consistency, ssot-registry completeness, and invariants across the changed files.\n\n${scopeBrief}\n\n${baselineBrief}\n\n` +
  `You OWN the doc domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as already validated and are NOT re-judged by another agent (a generic code-reviewer judging prose would be cross-domain). Flag only a finding you genuinely cannot resolve as confidence < 80.\n\n` +
  `Return findings (domain almost always "doc"). Use the finding schema fields.`

const apiPrompt =
  `API / data-model / performance / cost defect review over the batch diff, per ${protocolRef} Step F.3 (api-perf-cost-auditor row). Look for unbounded reads, N+1, missing pagination, contract drift, and cost regressions.\n\n${scopeBrief}\n\n${baselineBrief}\n\n` +
  `You OWN the api/perf/data domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as already validated and are NOT re-judged by another agent. Flag only a finding you genuinely cannot resolve as confidence < 80.\n\n` +
  `Return findings with domain in {code, perf, migration, security}. Use the finding schema fields.`

const qaPrompt =
  `Run MECHANICAL GATES ONLY over the batch scope, per ${protocolRef} Step F.3 (qa-sentinel row): lint, type-check, the full test suite, build, dependency audit, and markdownlint as applicable to this project. Do NOT read source for code findings, do NOT emit severities — return only a PASS/FAIL/SKIP gate table.\n\nWorktree: ${a.worktreePath || '(cwd)'}\nChanged files:\n${scope.join('\n')}`

// F-041 — single-card batch: the per-card review (Phase 3) already ran doc-reviewer +
// api-perf-cost-auditor over these exact files, and a 1-card batch has NO cross-card
// conflict to surface. Keep ONLY the cross-model Codex pass (its unique value — a different
// model finds different bugs) + qa-sentinel gates; skip the Claude-agent duplicates.
const slim = a.singleCard === true
// qa-sentinel always runs — the merge integrity gate reads its PASS/FAIL table.
const reviewThunks = [
  () => agent(qaPrompt, { label: 'qa-sentinel', phase: 'Review', agentType: 'qa-sentinel', schema: GATES_SCHEMA }).then((r) => ({ kind: 'qa', r })),
]
if (!slim) {
  reviewThunks.unshift(() => agent(docPrompt, { label: 'doc-reviewer', phase: 'Review', agentType: 'doc-reviewer', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'doc', r })))
}
// Codex thunk runs ONLY when the pre-flight resolved the companion (else: no wasted agent).
if (codexResolved) {
  reviewThunks.unshift(() => agent(codexPrompt, { label: 'codex', phase: 'Review', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', r })))
}
// api-perf-cost-auditor: skipped when no API/data files OR on a slim single-card pass.
if (!slim && a.hasApiDataFiles !== false) {
  reviewThunks.push(() => agent(apiPrompt, { label: 'api-perf-cost-auditor', phase: 'Review', agentType: 'api-perf-cost-auditor', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'api', r })))
} else {
  log(slim
    ? 'Review: single-card batch — doc-reviewer + api-perf skipped (already run per-card); kept cross-model Codex + qa gates.'
    : 'Review: api-perf-cost-auditor skipped (no API/data files in scope).')
}

const reviewResults = (await parallel(reviewThunks)).filter(Boolean)

// ---- Fan-in (F.4 step 9): collect findings + Codex fallback branch ----------
let raw = []
let gateTable = []
let codexEngine = codexResolved ? 'codex' : 'code-reviewer (fallback)'
let codexRan = false
for (const item of reviewResults) {
  if (item.kind === 'codex') {
    if (item.r && item.r.codexAvailable && Array.isArray(item.r.findings)) {
      codexRan = true
      raw.push(...item.r.findings.map((f) => ({ ...f, source: 'codex', preValidated: true })))
    } else {
      // Resolved but the run itself failed (CODEX_NOT_FOUND written / empty after timeout).
      log('Review: Codex companion resolved but its review did not complete — falling back to code-reviewer.')
    }
  } else if (item.kind === 'qa') {
    gateTable = (item.r && item.r.gates) || []
  } else if (item.r && Array.isArray(item.r.findings)) {
    // doc-reviewer / api-perf-cost-auditor own their domain and FP-check their OWN findings in
    // the finding pass (their prompts mandate it) → already validated, no cross-domain re-judge.
    raw.push(...item.r.findings.map((f) => ({ ...f, source: item.kind, preValidated: true })))
  }
}

// F-040 — ONE deterministic fallback: either the pre-flight found no companion, or it ran
// but did not complete. Either way the primary code review must still happen (F.4 step 8).
if (!codexRan) {
  codexEngine = 'code-reviewer (fallback)'
  const fb = await agent(
    `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied.`,
    { label: 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
  )
  // the fallback code-reviewer IS the primary code-review engine here and applied its own FP
  // check (prompt above) → trusted, exactly like Codex. A SECOND code-reviewer pass over its own
  // findings would be self-judging (no model diversity) — preValidated short-circuits it.
  if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: true })))
}

// ───────────────────────────────────────────────────────────────────────────
// Phase Verify (F.4) — specialist-owned validation (NOT a generic code-reviewer re-judge).
//   Each domain specialist owns its lane end-to-end: it FP-checks its OWN findings in the
//   finding pass, so codex / doc-reviewer / api-perf-cost-auditor / code-reviewer-fallback
//   findings arrive preValidated → VERIFIED with no second pass. This removes two flaws of the
//   old "spawn code-reviewer for every low-confidence finding": (a) code-reviewer judging a doc
//   or api finding is CROSS-DOMAIN (wrong specialist); (b) code-reviewer judging the fallback
//   code-reviewer's OWN findings is SELF-JUDGING (no model diversity). The residual branch only
//   fires for a finding a specialist explicitly left UNRESOLVED (confidence < 80, not
//   preValidated): it is routed to the finding's DOMAIN specialist; if that specialist IS the
//   originating finder, it already had its pass → surface as NEEDS_MANUAL_CONFIRMATION (never a
//   self-judge, never a silent drop).
// ───────────────────────────────────────────────────────────────────────────
phase('Verify')
const classified = (await parallel(raw.map((f) => () => verifyFinding(f)))).filter(Boolean)

// Route a finding to the specialist that OWNS its domain (never a generic code-reviewer for
// doc/api/security). Kept in sync with new2-resolve.js normDomain() routing buckets.
function domainVerifier(domain) {
  const d = String(domain || 'code').toLowerCase()
  if (/doc|wiki|ssot|readme/.test(d)) return 'doc-reviewer'
  if (/sec|auth|secret|rls|migrat|schema|ddl|\bsql\b/.test(d)) return 'security-reviewer'
  if (/perf|cost|\bapi\b|data|latency|throughput|n\+1/.test(d)) return 'api-perf-cost-auditor'
  if (/\btest|qa\b|spec|coverage/.test(d)) return 'qa-sentinel'
  return 'code-reviewer'
}

async function verifyFinding(f) {
  if (f.preValidated || (typeof f.confidence === 'number' && f.confidence >= 80)) {
    return { ...f, classification: 'VERIFIED' }
  }
  const verifier = domainVerifier(f.domain)
  if (verifier === f.source) {
    // the domain specialist IS the finder and already had its single pass — a second instance of
    // the same agent adds no diversity (self-judge). Surface to the human, do not drop.
    return { ...f, classification: 'NEEDS_MANUAL_CONFIRMATION' }
  }
  const v = await agent(
    `Adversarially validate this ${f.domain || 'code'} finding as the DOMAIN specialist over the cited file:line. Default to FALSE_POSITIVE if the evidence does not hold; use NEEDS_MANUAL_CONFIRMATION only when you genuinely cannot decide from the code.\n\n` +
      `finding_id: ${f.finding_id}\nseverity: ${f.severity}\ntitle: ${f.title}\nevidence: ${f.evidence}\ndomain: ${f.domain}\nsecurity-sensitive paths: ${highRisk.join(', ') || '(none configured)'}`,
    { label: `verify:${f.finding_id}`, phase: 'Verify', agentType: verifier, schema: VERDICT_SCHEMA }
  )
  return { ...f, classification: (v && v.classification) || 'NEEDS_MANUAL_CONFIRMATION' }
}

// ---- Result: drop FALSE_POSITIVE, keep VERIFIED + NEEDS_MANUAL (F.4 step 9) --
const findings = classified.filter((f) => f.classification !== 'FALSE_POSITIVE')
const summary = {
  total: raw.length,
  verified: classified.filter((f) => f.classification === 'VERIFIED').length,
  falsePositive: classified.filter((f) => f.classification === 'FALSE_POSITIVE').length,
  needsManual: classified.filter((f) => f.classification === 'NEEDS_MANUAL_CONFIRMATION').length,
  blockers: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
  highs: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
  failingGates: gateTable.filter((g) => g.status === 'FAIL').map((g) => g.gate),
}
log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s). Engine: ${codexEngine}.`)

return { codexEngine, findings, gateTable, summary }

function emptySummary() {
  return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [] }
}
