Adds the fourth AI detective in the Investigation Bureau runtime: Bruce
Schneier, who attacks an existing hypothesis as a red-team operator.
Runtime:
- prompts/schneier.md — discipline (don't disprove, just attack;
structured output with hidden_assumptions, failure_modes,
alternative_explanations, recommended_tests, verdict_one_sentence;
severity ∈ {low, medium, high}; emit INSUFFICIENT_HYPOTHESIS when
the input is too thin)
- src/detectives/schneier.ts — reads the hypothesis row + evidence
chain (joined via evidence_refs FK), feeds Claude with the
arguments + verbatim quotes, parses strict JSON object
- src/tools/write_red_team_review.ts — UPDATEs hypotheses.reviewed_by
+ updated_at; APPENDS (or replaces if re-reviewed) a structured
"## Red-team review (Schneier · X severity)" section to
case/hypotheses/H-NNNN.md. Caps each list at 5 entries × 240 chars,
validates verdict ≤ 280 chars.
- orchestrator: new `red_team_review` kind dispatching to runSchneier
Chat + UI:
- request_investigation gains kind=red_team_review + hypothesis_id arg
(validated against H-NNNN regex); detective auto-resolves to schneier
- chat-bubble inline card paints Schneier in red (#ff3344)
- /jobs/[id] page swaps title/subtitle/tone per detective; the
"Question" label becomes "Hypothesis under attack" for red_team_review
New /h/[hypothesisId] page (hypothesis dossier):
- Server-rendered from public.hypotheses + public.evidence (joined
via evidence_refs FK + chunk lookup)
- Header: ID + creator + reviewer (highlighted when Schneier has
visited), position as headline, question subtitle, Tetlock band
- Prior + posterior bars with Δ-delta indicator
- Argument grid: argument_for (green) vs argument_against (pink)
side-by-side with [[wiki-link]] auto-linking to source chunks
- Evidence chain: each E-NNNN with Grade A/B/C badge, verbatim
blockquote, link to source page
- Red-team review panel: parses the markdown section in the case
file (severity badge, verdict, 4 bullet panels for
hidden_assumptions / failure_modes / alternative_explanations /
recommended_tests). Empty state when not yet reviewed.
RedTeamRequestButton client component + POST /api/h/[id]/red-team —
authenticated user can trigger Schneier in one click; UI swaps to
"acompanhar" link to /jobs/[id] once queued.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* RedTeamRequestButton — POSTs to /api/h/[id]/red-team to enqueue a Schneier
|
|
* red_team_review job. Shows the job_id + link to /jobs/[id] once submitted.
|
|
*/
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { ArrowUpRight } from "lucide-react";
|
|
|
|
export function RedTeamRequestButton({
|
|
hypothesisId,
|
|
alreadyReviewed,
|
|
}: { hypothesisId: string; alreadyReviewed: boolean }) {
|
|
const [pending, setPending] = useState(false);
|
|
const [jobId, setJobId] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function requestReview() {
|
|
setPending(true);
|
|
setError(null);
|
|
try {
|
|
const r = await fetch(`/api/h/${hypothesisId}/red-team`, { method: "POST" });
|
|
const d = (await r.json()) as { job_id?: string; error?: string; message?: string };
|
|
if (!r.ok || !d.job_id) {
|
|
setError(d.error || d.message || `HTTP ${r.status}`);
|
|
return;
|
|
}
|
|
setJobId(d.job_id);
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
if (jobId) {
|
|
return (
|
|
<Link
|
|
href={`/jobs/${jobId}`}
|
|
target="_blank"
|
|
className="inline-flex items-center gap-1 text-[11px] font-mono text-[#ff3344] hover:underline"
|
|
>
|
|
Schneier em ação — acompanhar <ArrowUpRight size={11} />
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={requestReview}
|
|
disabled={pending}
|
|
className="px-3 py-1 rounded border border-[#ff3344] text-[11px] font-mono text-[#ff3344] hover:bg-[rgba(255,51,68,0.06)] disabled:opacity-40 disabled:cursor-not-allowed"
|
|
>
|
|
{pending ? "Enfileirando…" : alreadyReviewed ? "Re-revisar (Schneier)" : "Acionar Schneier (red-team)"}
|
|
</button>
|
|
{error && (
|
|
<span className="text-[10px] font-mono text-[#ff6ec7]">{error}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|