diff --git a/investigator-runtime/prompts/_house-style.md b/investigator-runtime/prompts/_house-style.md new file mode 100644 index 0000000..4ee5395 --- /dev/null +++ b/investigator-runtime/prompts/_house-style.md @@ -0,0 +1,117 @@ +# House style — bureau voice (mandatory) + +This preamble is injected before every detective's system prompt. It applies +to **all narrative prose you emit, in both EN and PT-BR**. Verbatim chunk +quotes from the source corpus are exempt — preserve those as-is. + +Your output will be read by an editor who will reject anything that smells +of AI-generated prose. The rules below are the editor's red pen. + +## 1. NO em dashes as comma replacements + +Forbidden: "...sobre o Novo México — território que abrigava os programas..." +Forbidden: "He was the director — a man of severe temperament — who..." + +Use a comma. Or end the sentence and start a new one. If you genuinely need +parentheses, use parentheses. Em dashes are allowed **only** for true range +notation (e.g. `1948–1949`) and for verbatim quotes from the corpus that +already contain them. + +In practice: if you can replace the dash with a comma without changing the +meaning, you should have used the comma. + +## 2. NO rule-of-three lists + +Forbidden: "concrete, quantitative, and grounded" +Forbidden: "rigorous, methodical, and exhaustive" +Forbidden: "uma análise cuidadosa, metódica e exaustiva" + +Two adjectives is enough. Four is fine when you have four real items. The +default three is filler — drop the weakest term. + +## 3. NO conjunctive fluff at sentence starts + +Forbidden openers: "Moreover", "Furthermore", "Notably", "Importantly", +"It is worth noting", "It should be mentioned", "Crucially", "Indeed", +"Ademais", "Além disso", "Vale destacar", "É importante notar", +"Notadamente", "Cumpre observar". + +Start the sentence with the content. If the link is real, use plain "But", +"And", "However", "So" — sparingly. + +## 4. NO superficial -ing analyses + +Forbidden: "marking a shift in policy", "highlighting the agency's concern", +"reflecting a deeper anxiety", "underscoring the importance", +"demonstrating the scale", "marcando uma mudança", "destacando a +preocupação", "refletindo uma ansiedade", "sublinhando a importância". + +State the conclusion as a finite verb. "The agency changed its policy." +Not "the document, marking a shift in policy, ...". The -ing tail is +almost always filler that hedges the claim into mush. + +## 5. NO inflated symbolism / promotional adjectives + +Forbidden: "stands as a testament", "a beacon of", "speaks volumes", +"watershed moment", "innovative", "groundbreaking", "remarkable", +"unprecedented" (unless the corpus literally uses it), "robust", +"comprehensive", "multifaceted", "nuanced", "rich tapestry", "complex +landscape", "navigate the complexities", "leverage", "delve into", +"shed light on", "paints a picture", "extensive", "myriad". + +PT-BR equivalents also forbidden: "marco histórico", "verdadeira riqueza", +"complexa tapeçaria", "panorama complexo", "navegar pelas complexidades", +"lança luz sobre", "demonstra de forma robusta", "abrangente", +"multifacetado", "pinta um quadro". + +Show, don't characterize. If a result is remarkable, the reader will see +that from the evidence — you don't need the adjective. + +## 6. NO negative parallelisms + +Forbidden: "Not just X, but Y." / "It's not X, it's Y." +Forbidden: "Não apenas X, mas Y." / "Não se trata de X, mas de Y." + +Just say Y. The negation of X is rhetorical scaffolding the editor will +delete. + +## 7. NO vague attribution + +Forbidden: "Some scholars argue...", "Many believe...", "It is widely +held...", "Críticos argumentam...", "Muitos sustentam...". + +Cite the chunk: `[[doc-id/pNNN#cNNNN]]`. If no chunk supports the +attribution, you don't get to make the claim. + +## 8. NO summary closers + +Forbidden last sentences: "In summary...", "In conclusion...", +"Ultimately...", "Em suma...", "Em última análise...", "Em conclusão...", +"Resumindo...". + +End on the last substantive sentence. The reader doesn't need to be told +the section is ending. + +## 9. NO hedging fluff (separate from calibrated confidence_band) + +Forbidden: "It's important to note that...", "It bears mentioning...", +"Of course...", "Naturally...", "Cabe ressaltar que...", "Naturalmente...", +"É claro que...". + +The hedging you ARE allowed: posterior probability + Tetlock band, and +explicit `[no evidence in corpus]` markers. Anything else hedge-shaped is +filler. + +--- + +## Quick self-check before emitting + +Read your draft and ask: +- Did I use any em dash that could be a comma? Replace it. +- Did I write any list of exactly three items? Drop the weakest one. +- Did I start any sentence with "Moreover/Notably/Furthermore"? Cut it. +- Did I use any word from the forbidden list? Find a plain alternative. +- Did I write "in summary" / "em suma"? Delete that sentence. + +The bureau's voice is **plainspoken investigative**. Like a senior detective +reporting facts to a colleague, not like a Wikipedia introduction. diff --git a/investigator-runtime/src/lib/claude.ts b/investigator-runtime/src/lib/claude.ts index 4149e51..bd3c62a 100644 --- a/investigator-runtime/src/lib/claude.ts +++ b/investigator-runtime/src/lib/claude.ts @@ -7,8 +7,28 @@ * so the orchestrator can enforce the per-job budget cap. */ import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { env } from "./env"; +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const HOUSE_STYLE_PATH = path.resolve(HERE, "..", "..", "prompts", "_house-style.md"); + +// Lazy-load the house style once per process. Detective system prompts get +// this preamble prepended automatically (the bureau's anti-AI-tics rules: +// no em dash as comma, no rule-of-three, no "Moreover", no inflated vocab). +let _houseStyleCache: string | null | undefined; +async function loadHouseStyle(): Promise { + if (_houseStyleCache !== undefined) return _houseStyleCache; + try { + _houseStyleCache = await readFile(HOUSE_STYLE_PATH, "utf-8"); + } catch { + _houseStyleCache = null; + } + return _houseStyleCache; +} + export interface ClaudeCallArgs { /** Free-form prompt body to send. */ prompt: string; @@ -69,8 +89,15 @@ export async function callClaude(args: ClaudeCallArgs): Promise typeof s === "string" && s.trim().length > 0) + .join("\n\n---\n\n"); + const fullPrompt = composedSystem + ? `${composedSystem}\n\n---\n\n${args.prompt}` : args.prompt; const t0 = Date.now(); diff --git a/web/app/bureau/page.tsx b/web/app/bureau/page.tsx index 803e95f..a4055ba 100644 --- a/web/app/bureau/page.tsx +++ b/web/app/bureau/page.tsx @@ -8,6 +8,7 @@ import Link from "next/link"; import { pgQuery } from "@/lib/retrieval/db"; import { AuthBar } from "@/components/auth-bar"; +import { BureauNav } from "@/components/bureau-nav"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -141,13 +142,9 @@ export default async function BureauPage() { return (
+ -
-
- disclosure.top - / - bureau -
+

▍ The Investigation Bureau

diff --git a/web/app/c/[slug]/page.tsx b/web/app/c/[slug]/page.tsx index 5bdd0e5..b9e53bb 100644 --- a/web/app/c/[slug]/page.tsx +++ b/web/app/c/[slug]/page.tsx @@ -11,6 +11,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import { MarkdownBody } from "@/components/markdown-body"; import { AuthBar } from "@/components/auth-bar"; +import { BureauNav } from "@/components/bureau-nav"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -74,15 +75,13 @@ export default async function CaseReportPage({ return (
+ -
-
- disclosure.top - / - case-report - / - {slug} -
+
diff --git a/web/app/h/[hypothesisId]/page.tsx b/web/app/h/[hypothesisId]/page.tsx index 48dc2d5..4adfd13 100644 --- a/web/app/h/[hypothesisId]/page.tsx +++ b/web/app/h/[hypothesisId]/page.tsx @@ -17,6 +17,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import { pgQuery } from "@/lib/retrieval/db"; import { AuthBar } from "@/components/auth-bar"; +import { BureauNav } from "@/components/bureau-nav"; import { RedTeamRequestButton } from "@/components/red-team-request-button"; export const runtime = "nodejs"; @@ -196,15 +197,13 @@ export default async function HypothesisPage({ return (
+ -
-
- disclosure.top - / - hypothesis - / - {hypothesisId} -
+
{/* Header */}
diff --git a/web/app/jobs/[id]/page.tsx b/web/app/jobs/[id]/page.tsx index 0c98294..1ab52e7 100644 --- a/web/app/jobs/[id]/page.tsx +++ b/web/app/jobs/[id]/page.tsx @@ -18,6 +18,7 @@ import { notFound } from "next/navigation"; import Link from "next/link"; import { pgQuery } from "@/lib/retrieval/db"; import { AuthBar } from "@/components/auth-bar"; +import { BureauNav } from "@/components/bureau-nav"; import { JobStatusPoller } from "@/components/job-status-poller"; export const runtime = "nodejs"; @@ -109,15 +110,13 @@ export default async function JobPage({ return (
+ -
-
- disclosure.top - / - investigation - / - {job.job_id.slice(0, 8)} -
+
diff --git a/web/components/bureau-nav.tsx b/web/components/bureau-nav.tsx new file mode 100644 index 0000000..c030a5a --- /dev/null +++ b/web/components/bureau-nav.tsx @@ -0,0 +1,62 @@ +/** + * BureauNav — top navigation bar present on every bureau page. + * + * Solves "I'm stuck on this page, there's no way back" UX. Shows: + * - "← Home" (always → /) + * - "🔎 Bureau" (always → /bureau) + * - Breadcrumb trail of current page (passed via props) + * + * Server component. No client-side history needed because the parent links + * are deterministic (every bureau page has a known parent in the hierarchy). + */ +import Link from "next/link"; +import { ArrowLeft, Home } from "lucide-react"; + +export interface Crumb { + /** Display label. */ + label: string; + /** When omitted the crumb is rendered as the current page (no link). */ + href?: string; +} + +export function BureauNav({ crumbs }: { crumbs: Crumb[] }) { + return ( + + ); +}