export const meta = {
  name: 'new2-resolve',
  description:
    "Self-healing resolution pass for the autonomous new2 batch workflow. Called by new2 whenever a deterministic gate would otherwise need a human: a card fail/blocker (ac-unmet | blocker | qa-fail | e2e-blocked | merge-blocker) or a legitimate scope-EXPANDING finding (scope-expansion). Tier-1 targeted fix with a TERMINAL short-circuit (skips the costly multi-attempt when the problem is impossible-by-definition, verified not trusted), then a judged multi-attempt; a MANDATORY adversarial judge cross-checks every verified claim against the real diff (prevents fabricated success) whenever the judge is a DIFFERENT specialist than the fixer. When fixer === judge (doc→doc-reviewer, security→security-reviewer) the second pass would just judge its own work — no cross-model diversity, pure waste — so the judge AND the terminal-verdict ratification are skipped (the reviewer-writer self-verifies in its single pass). Specialized per domain (doc→doc-reviewer single pass, ui→ui-expert fix + code-reviewer judge, security→security-reviewer single pass, perf→api-perf-cost-auditor judge). Terminal is a tracked follow-up. Accepts a `findings` list (batched per area). Uses agent()/parallel() only — no nested workflows.",
  phases: [
    { title: 'Diagnose', detail: 'classify + terminal short-circuit + scope-expansion boundary' },
    { title: 'Repair', detail: 'targeted fix, then judged multi-attempt if needed' },
    { title: 'Verify', detail: 'mandatory adversarial judge + JS cross-check of verified files' },
  ],
}

// ───────────────────────────────────────────────────────────────────────────
// args contract (from new2.js resolve()):
//   kind, cardId, evidence, worktreePath, mayEditPaths[], scopeFiles[], domain,
//   refModulesBase, config, ts, findings[] (batched: [{kind,evidence,domain}])
// return: { status:'resolved'|'followup'|'fatal', followupCard?, reason?, outOfScopeFindings? }
// ───────────────────────────────────────────────────────────────────────────

// F-004 — tolerate args delivered as a JSON string.
let a = args || {}
if (typeof a === 'string') { try { a = JSON.parse(a) } catch (_) { a = {} } }
const kind = a.kind || 'blocker'
const card = a.cardId || 'CARD'
const evidence = a.evidence || ''
const worktree = a.worktreePath || '(cwd)'
const mayEdit = Array.isArray(a.mayEditPaths) ? a.mayEditPaths : []
const scopeFiles = Array.isArray(a.scopeFiles) ? a.scopeFiles : []
// F-038 — review agents emit FREEFORM domain strings (e.g. 'documentation', 'frontend').
// Normalize to the routing buckets the fixer/judge maps key on, so a doc finding never
// falls through to the costly `coder` (the v4.17.0 'documentation'→coder regression).
// G6: the doc token set below MUST stay in sync with isDocDomain() in new2.js (which picks
// the doc MAY-EDIT) — drift would route a finding to doc-reviewer but with code ownership.
function normDomain(d) {
  const s = String(d || 'code').toLowerCase()
  if (/doc|wiki|ssot|readme/.test(s)) return 'doc'
  if (/ui|frontend|front-end|design|css|visual|component/.test(s)) return 'ui'
  if (/sec|auth|secret|rls|tenant|xss|csrf|injection/.test(s)) return 'security'
  if (/migrat|schema|ddl|\bsql\b/.test(s)) return 'migration'
  if (/perf|cost|n\+1|latency|throughput/.test(s)) return 'perf'
  if (/\btest|qa\b|spec|coverage/.test(s)) return 'test'
  return 'code'
}
const domain = normDomain(a.domain || 'code')
const findings = Array.isArray(a.findings) && a.findings.length ? a.findings : [{ kind, evidence, domain }]
const REF = (a.refModulesBase || '.claude/skills/new/references').replace(/\/$/, '')
const cfg = a.config || {}
const paths = cfg.paths || {}
const backlogDir = paths.backlog_dir || 'backlog'
const protectedDomain = domain === 'security' || domain === 'migration'
// v4.30.0 — when the new2 Cross-Card Integration Pass RE-runs resolve on an already-tracked
// residual (with widened ownership), a failure must NOT mint a SECOND follow-up card: the
// original residual is still tracked by the caller. noFollowup makes materialiseFollowup return
// the verdict without spawning prd-card-writer (no duplicate card, no wasted spawn).
const noFollowup = a.noFollowup === true

// F-019 — bounded immediate retry on transient errors (shared pattern; no import allowed).
const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
function isTransient(e) { return TRANSIENT.test(String((e && e.message) || e || '')) }
async function agentSafe(prompt, opts, maxAttempts) {
  const cap = maxAttempts || 3
  let lastErr = null
  for (let i = 0; i < cap; i++) {
    try {
      const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
      return await agent(prompt, o)
    } catch (e) { lastErr = e; if (!isTransient(e)) throw e }
  }
  const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr)); err.transientExhausted = true; throw err
}

const FIX_SCHEMA = {
  type: 'object', required: ['applied', 'verified'], additionalProperties: true,
  properties: {
    applied: { type: 'boolean' },
    verified: { type: 'boolean', description: 'true only if you re-ran the originating gate and it passed' },
    // F-008 — terminal short-circuit (impossible-by-definition). Not trusted blindly.
    terminal: { type: 'boolean', description: 'true if no in-MAY-EDIT fix can ever satisfy the gate' },
    terminalReason: { enum: ['out-of-ownership', 'baseline-not-reached', 'owner-gated', 'not-a-code-defect', ''] },
    remedyFiles: { type: 'array', items: { type: 'string' }, description: 'files the real fix would require editing (for out-of-ownership JS verification)' },
    crashed: { type: 'boolean' },
    // F-022 — incidental findings outside this card's MAY-EDIT (so they are not dropped).
    outOfScopeFindings: { type: 'array', items: { type: 'object', additionalProperties: true } },
    note: { type: 'string' },
  },
}
const JUDGE_SCHEMA = {
  type: 'object', required: ['best'], additionalProperties: false,
  properties: {
    best: { type: 'number', description: '1-based index of the soundest verified attempt, or 0 if none truly holds' },
    // F-015 — the files the judge INDEPENDENTLY confirmed exist and changed (cross-checked in JS).
    verifiedFiles: { type: 'array', items: { type: 'string' } },
    rationale: { type: 'string' },
  },
}
const TERMINAL_JUDGE_SCHEMA = {
  type: 'object', required: ['confirmed'], additionalProperties: false,
  properties: { confirmed: { type: 'boolean', description: 'true if the terminal verdict genuinely holds' }, rationale: { type: 'string' } },
}
const FOLLOWUP_SCHEMA = {
  type: 'object', required: ['created', 'followupCard'], additionalProperties: true,
  properties: { created: { type: 'boolean' }, followupCard: { type: 'string' }, note: { type: 'string' } },
}

// F-024 — domain-specialized fixer + judge (full map; reviewer-owns-its-domain — a doc
// finding is fixed by doc-reviewer, a security finding by security-reviewer, never coder).
const fixerAgent = ({ doc: 'doc-reviewer', ui: 'ui-expert', security: 'security-reviewer' })[domain] || 'coder'
// Specialization integrity (v4.26.1) — the judge is the VERIFICATION specialist of the
// finding's domain: doc fixes judged by doc-reviewer (code-reviewer judging prose was
// cross-domain), test fixes by qa-sentinel (THE test specialist). `ui` and `code` stay with
// code-reviewer: the judge verifies a CODE change, which is code-reviewer's charter
// (including DS-coherence rule 8 for UI). Fixer and judge of the same type are still two
// independent instances — the judge prompt is adversarial and greps the files itself.
const judgeAgent = (domain === 'security' || domain === 'migration') ? 'security-reviewer'
  : domain === 'perf' ? 'api-perf-cost-auditor'
  : domain === 'doc' ? 'doc-reviewer'
  : domain === 'test' ? 'qa-sentinel'
  : 'code-reviewer'
// Anti-self-judging — when the fixer and the adversarial judge resolve to the SAME agent type
// (doc→doc-reviewer, security→security-reviewer), the "judge" would just review its own work:
// no cross-model diversity, pure waste of tokens and time. These are reviewer-writer specialists
// that own their domain and self-verify (re-run the gate) inside the fix pass, so we trust the
// single pass and skip both the judge cross-check and the terminal-verdict ratification. Domains
// where fixer ≠ judge (ui/perf/test/code/migration) keep the mandatory adversarial cross-check —
// there the judge is a genuinely independent specialist. Condition is structural (not a domain
// allowlist), so a future fixer/judge map change can't silently re-introduce self-judging.
const selfJudges = fixerAgent === judgeAgent

const findingsBlock = findings.map((f, i) => `  ${i + 1}. [${f.kind || kind}/${f.domain || domain}] ${f.evidence}`).join('\n')
const brief = [
  `Worktree: ${worktree} (cd into it; run all gates/git yourself).`,
  `Card: ${card}`,
  `Problem kind: ${kind}`,
  findings.length > 1 ? `Findings to fix (batched — same fix-area):\n${findingsBlock}` : `Evidence: ${evidence}`,
  `MAY-EDIT (file-ownership — do NOT touch anything else): ${mayEdit.join(', ') || '(card scope)'}`,
  `Scope files for re-verify: ${scopeFiles.join(', ') || '(diff)'}`,
  `Domain: ${domain}${protectedDomain ? ' (PROTECTED — security/migration: never inline-bypass; the right specialist MUST apply)' : ''}`,
  `Reference modules (Read for exact semantics): ${REF}/`,
  `UI re-verify obligation: if your fix touches a UI file, re-run the Phase 2.6 E2E review (per ${REF}/review-cycle.md) before declaring verified.`,
  `If the fix requires editing files OUTSIDE MAY-EDIT, return terminal:true terminalReason:'out-of-ownership' remedyFiles:[...] (do NOT edit them). If the remedy is an owner-gated infra action, terminal:true terminalReason:'owner-gated'. Report any incidental defect outside MAY-EDIT in outOfScopeFindings.`,
].filter(Boolean).join('\n')

// JS-deterministic ownership check (F-008 anti-escape-hatch): are remedy files ⊆ MAY-EDIT?
function filesInScope(files) {
  if (!Array.isArray(files) || !files.length) return false
  if (!mayEdit.length) return false
  return files.every((f) => mayEdit.some((m) => String(f).includes(m) || m.includes(String(f))))
}
// F-033 — fabrication guard for the judge cross-check: a REAL fix touched AT LEAST ONE
// owned file. The judge naturally also lists adjacent changed files (e.g. the code a doc
// describes) — that is NOT fabrication, so `.every()` here over-rejected a sound fix and
// forced a wasted Tier-2 fan-out. `.some()` rejects only a verdict that confirmed ZERO
// owned files (pure phantom).
function someInScope(files) {
  if (!Array.isArray(files) || !files.length || !mayEdit.length) return false
  return files.some((f) => mayEdit.some((m) => String(f).includes(m) || m.includes(String(f))))
}
function collectOOS(...attemptObjs) {
  const out = []
  for (const o of attemptObjs) for (const x of (o && o.outOfScopeFindings) || []) out.push(x)
  return out
}

phase('Diagnose')

// NOTE: new2.js never routes 'baseline-fail' (→ batchFatal at pre-flight) nor 'agent-crash'
// (→ crashResult marks the card 'failed' + residual) here — a crash happens BEFORE runCard's
// commit phase, so a 'resolved' verdict could never be committed/merged. Those self-healing
// branches were removed (v4.17.2 G1) as unreachable dead code; the residual path is the SSOT.

// ---- scope-expansion (Asse 2): deterministic boundary ---
if (kind === 'scope-expansion') {
  let decide = null
  try {
    decide = await agentSafe(
      `A review finding EXPANDS scope beyond card ${card}'s acceptance criteria. Decide per the deterministic boundary (no line-count threshold):\n` +
        `INTEGRATE NOW iff ALL: (1) fix stays inside MAY-EDIT; (2) domain NOT security/migration; (3) no NEW user-facing AC. Else MATERIALISE A FOLLOW-UP.\n\n${brief}\n\n` +
        `If INTEGRATE: apply (you are ${fixerAgent}), re-run lint+tsc, return applied:true verified:true. If FOLLOW-UP: applied:false verified:false note:'needs-followup: <why>'.`,
      { label: `resolve:scope:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
    )
  } catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during scope-expansion', deferralClass: 'outage', outOfScopeFindings: [] }; throw e }
  if (decide && decide.verified) {
    const ok = await judgeVerify([{ i: 1, r: decide }])
    if (ok.ok) { log('scope-expansion integrated within ownership.'); return { status: 'resolved', outOfScopeFindings: collectOOS(decide) } }
  }
  return await materialiseFollowup('scope-expansion', (decide && decide.note) || 'outside ownership / new AC / protected', collectOOS(decide), 'scope-expansion')
}

// ───────────────────────────────────────────────────────────────────────────
// Asse 1 — Tier-1 (with terminal short-circuit) → judged multi-attempt → follow-up.
// ───────────────────────────────────────────────────────────────────────────
const gateHint = {
  'ac-unmet': `Implement the unmet acceptance criterion EXACTLY, per ${REF}/completeness.md Phase 2.5b. Re-verify against the code.`,
  blocker: `Fix the blocking finding per ${REF}/codex-gate.md / ${REF}/completeness.md. Re-run the failing gate.`,
  'qa-fail': `Fix the failing QA gates per ${REF}/review-cycle.md Phase 3.5 — fix the code, not the tests. Re-run qa-sentinel.`,
  'e2e-blocked': `Fix so the E2E review passes per ${REF}/review-cycle.md Phase 2.6. Re-run /e2e-review.`,
  'merge-blocker': `Fix the verified BLOCKER/HIGH from the final review per ${REF}/final-review.md F.5. Re-verify.`,
}[kind] || `Fix the blocker and re-verify.`

phase('Repair')
let attempt = null
try {
  attempt = await agentSafe(
    `Tier-1 targeted repair for card ${card} (${kind}).\n\n${brief}\n\n${gateHint}\n\nApply the minimal correct fix within MAY-EDIT only. Re-run the originating gate and report verified honestly (never claim verified without re-running it).`,
    { label: `resolve:${kind}:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
  )
} catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during tier-1', deferralClass: 'outage', outOfScopeFindings: [] }; throw e }

// F-008 — terminal short-circuit, verified not trusted.
if (attempt && attempt.terminal) {
  const tr = attempt.terminalReason || ''
  let confirmed = false
  if (tr === 'out-of-ownership') {
    confirmed = !filesInScope(attempt.remedyFiles) // genuinely terminal iff remedy files are NOT in MAY-EDIT
  } else if (selfJudges) {
    // fixer === judge (doc→doc-reviewer, security→security-reviewer): ratifying the terminal
    // verdict with a SECOND instance of the same specialist is self-judging — no cross-model
    // diversity, pure waste. Trust the reviewer-writer's single pass; no adversarial re-run.
    confirmed = true
  } else {
    // owner-gated / not-a-code-defect / baseline-not-reached — ratify with the judge.
    try {
      const tj = await agentSafe(
        `A repair agent declared this problem TERMINAL (reason: ${tr}) — no in-MAY-EDIT code fix can satisfy it.\n\n${brief}\n\nIndependently verify: is that verdict genuinely correct, or is it an excuse to avoid work? Return confirmed:true only if it truly holds.`,
        { label: `resolve:terminal-judge:${card}`, phase: 'Verify', agentType: judgeAgent, schema: TERMINAL_JUDGE_SCHEMA }
      )
      confirmed = !!(tj && tj.confirmed)
    } catch (_) { confirmed = false }
  }
  if (confirmed) { log(`${kind} terminal (${tr}) — short-circuit to follow-up.`); return await materialiseFollowup(kind, `terminal: ${tr} — ${attempt.note || ''}`, collectOOS(attempt), tr || 'unresolved', attempt.remedyFiles) }
  log(`terminal verdict (${tr}) rejected — proceeding to multi-attempt.`)
}

// F-015 — a verified Tier-1 still goes through the mandatory judge cross-check.
if (attempt && attempt.verified) {
  const ok = await judgeVerify([{ i: 1, r: attempt }])
  if (ok.ok) { log(`${kind} resolved (tier 1, judge-confirmed).`); return { status: 'resolved', outOfScopeFindings: collectOOS(attempt) } }
  log('tier-1 verified rejected by judge cross-check — escalating.')
}

// Tier 2 — judged multi-attempt (budget-guarded; single specialist for protected domains).
const canFanOut = (typeof budget === 'undefined' || !budget.total || budget.remaining() > 60000)
let tier2OOS = []
if (canFanOut && !protectedDomain) {
  phase('Repair')
  // F-036 — the 3-angle fan-out (root-cause / call-site / reuse-utility) is code-bug-shaped.
  // For deterministic non-code domains (doc/test) it is 3× waste — a single targeted retry
  // is enough. Reserve the fan-out for code/perf/ui where the angles genuinely diverge.
  const codeShaped = !/^(doc|test)$/.test(domain)
  const angles = codeShaped ? [
    'Fix at the root cause (change the underlying logic/contract).',
    'Fix defensively at the call site (guard/validate without changing the contract).',
    'Fix by reusing an existing utility/pattern instead of new code.',
  ] : [
    'Apply the single correct fix — the residual is deterministic: re-derive the exact change and apply it.',
  ]
  const tries = (await parallel(angles.map((angle, i) => () =>
    agentSafe(
      `Tier-2 repair attempt #${i + 1} for card ${card} (${kind}), angle: ${angle}\n\n${brief}\n\n${gateHint}\n\nApply within MAY-EDIT, re-run the gate, report whether it passes. Work in isolation; the best attempt is selected.`,
      { label: `resolve:${kind}:${card}#${i + 1}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
    ).then((r) => ({ i: i + 1, r })).catch(() => null)
  ))).filter(Boolean)
  tier2OOS = collectOOS(...tries.map((t) => t.r))
  const verified = tries.filter((x) => x.r && x.r.verified)
  if (verified.length) {
    const ok = await judgeVerify(verified)
    if (ok.ok) { log(`${kind} resolved (tier 2, attempt #${ok.best}, judge-confirmed).`); return { status: 'resolved', outOfScopeFindings: collectOOS(attempt).concat(tier2OOS) } }
  }
} else if (protectedDomain) {
  log('protected domain — single specialist attempt only (no fan-out).')
} else {
  log('budget near target — skipping tier-2 fan-out.')
}

return await materialiseFollowup(kind, (attempt && attempt.note) || 'unresolved after repair tiers', collectOOS(attempt).concat(tier2OOS), 'unresolved')

// ───────────────────────────────────────────────────────────────────────────
// F-015/F-033 — mandatory adversarial judge + deterministic JS cross-check.
// The judge (cross-model code-reviewer / domain specialist) does its OWN grep and
// returns the files it independently confirmed changed; we cross-check ⊆ MAY-EDIT.
async function judgeVerify(verifiedAttempts) {
  if (!verifiedAttempts.length) return { ok: false, best: 0 }
  // fixer === judge (doc→doc-reviewer, security→security-reviewer): the reviewer-writer
  // already applied the fix AND self-verified (re-ran the gate) in the same pass. A SECOND
  // adversarial instance of the same specialist would just judge its own work — no cross-model
  // diversity, a pure waste of tokens and time. Trust the single pass; accept the first
  // verified attempt without re-spawning. Domains where fixer ≠ judge keep the cross-check.
  if (selfJudges) {
    const first = verifiedAttempts.find((v) => v.r && v.r.verified) || verifiedAttempts[0]
    return { ok: true, best: first.i }
  }
  let judge = null
  try {
    judge = await agentSafe(
      `Adversarially judge these repair attempts for card ${card} (${kind}). Each CLAIMS the gate now passes — verify INDEPENDENTLY (grep/read the files yourself; do NOT trust the claim). Pick the SOUNDEST (best minimal, least regression-prone); return 0 if NONE genuinely holds. Also return verifiedFiles = the files you independently confirmed exist AND changed.\n\nEvidence: ${evidence}\nAttempts claiming verified: ${verifiedAttempts.map((v) => `#${v.i}: ${(v.r && v.r.note) || ''}`).join(' | ')}`,
      { label: `resolve:judge:${card}`, phase: 'Verify', agentType: judgeAgent, schema: JUDGE_SCHEMA }
    )
  } catch (e) { if (e && e.transientExhausted) return { ok: false, best: 0 }; throw e }
  if (!judge || !(judge.best > 0)) return { ok: false, best: 0 }
  // Deterministic JS cross-check: the judge-confirmed files must be within MAY-EDIT.
  if (mayEdit.length && Array.isArray(judge.verifiedFiles) && judge.verifiedFiles.length && !someInScope(judge.verifiedFiles)) {
    log('judge verifiedFiles are ENTIRELY outside MAY-EDIT — rejecting (possible fabrication).')
    return { ok: false, best: 0 }
  }
  return { ok: true, best: judge.best }
}

// F-040 — `deferralClass` (4th arg) classifies WHY this became a follow-up so new2.js can
// decide whether the CARD should still commit. owner-gated / not-a-code-defect → the card's
// own code is complete; the residual is an external step (do NOT roll the card back). Anything
// else (unresolved code defect, out-of-ownership, baseline) → genuine block → rollback as before.
// The classifier flows back through resolve() in new2.js; never write it into the worktree.
async function materialiseFollowup(k, reason, oos, deferralClass, remedyFiles) {
  const cls = deferralClass || 'unresolved'
  const rf = Array.isArray(remedyFiles) ? remedyFiles : []
  // noFollowup (integration re-run): return the verdict WITHOUT minting a duplicate card.
  // remedyFiles rides the verdict so the caller can decide whether the remedy is in-batch.
  if (noFollowup) {
    log(`${k} still unresolved at integration — caller keeps the original residual (no duplicate card).`)
    return { status: 'followup', followupCard: null, reason, deferralClass: cls, outOfScopeFindings: oos || [], remedyFiles: rf }
  }
  let r = null
  try {
    // F-039 — backlog cards are owned by prd-card-writer (card-template + Rule C
    // review_profile + owner_agent + traceability), NOT a hand-written Haiku stub.
    // F-040 — the workflow agent runs cd'd into the worktree, so this write is BEST-EFFORT
    // (it rides the merge if the batch merges). The SKILL is the SSOT: it verifies/creates the
    // card in the MAIN repo post-run, so a non-merged batch never loses the follow-up.
    r = await agentSafe(
      `Create ONE follow-up backlog card so this residual is TRACKED, not dropped (per ${REF}/completeness.md Phase 2.5b option 3). You are prd-card-writer: apply your card-template, Rule C (review_profile), owner_agent routing, and traceability rules — do NOT emit a minimal stub.\n\n${brief}\nKind: ${k}\nResidual domain: ${domain}\nReason unresolved: ${reason}\n\n` +
        `Write ${backlogDir}/${card}-followup-<gate>.yml with status: TODO, derived from the residual: requirements + acceptance_criteria (the verbatim residual as ≥1 AC), owner_agent routed to the residual domain (${domain}), review_profile per Rule C, files_likely_touched ≥1 from the card ownership / remedy files. It MUST pass the /new pre-flight field check. Return the created card id.`,
      { label: `resolve:followup:${card}`, phase: 'Verify', agentType: 'prd-card-writer', schema: FOLLOWUP_SCHEMA }
    )
  } catch (e) {
    // F-020 — could not materialise (e.g. outage): return WITHOUT a followupCard so the
    // SKILL writes it from the offline-safe residual ledger. Never claim it was created.
    log(`follow-up materialisation failed (${String(e && e.message)}) — skill will reconcile.`)
    return { status: 'followup', followupCard: null, reason, deferralClass: cls, outOfScopeFindings: oos || [], remedyFiles: rf }
  }
  const followupCard = (r && r.created && r.followupCard) ? r.followupCard : null
  log(`${k} → follow-up ${followupCard || '(deferred to skill)'} (nothing dropped).`)
  return { status: 'followup', followupCard, reason, deferralClass: cls, outOfScopeFindings: oos || [], remedyFiles: rf }
}
