Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 20 additions & 23 deletions app/(public)/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,32 @@ export default function AboutPage() {
<div className="prose max-w-none space-y-8 text-text-secondary [&_h2]:font-sans [&_h2]:font-bold [&_h2]:text-text-primary [&_a]:text-accent [&_a:hover]:underline">
<p>
I&apos;m Royal Simpson Pinto, an AI and product engineer. I spent
months documenting real AI agent failures in production: deleted
data, wrong recipients, surprise bills, exposed credentials. That
public registry still exists on this site, and every guardrail in
my installs traces back to a case in it.
months documenting real AI agent failures in production: deleted data,
wrong recipients, surprise bills, exposed credentials. That public
registry still exists on this site, and every guardrail in my installs
traces back to a case in it.
</p>

<p>
The lesson from all those cases is simple. Agents fail when they
get unlimited access, no audit trail, and nobody approving the
risky calls. So that is exactly what my installs refuse to do:
scoped keys, every action logged, and a human approval step for
anything irreversible.
The lesson from all those cases is simple. Agents fail when they get
unlimited access, no audit trail, and nobody approving the risky
calls. So that is exactly what my installs refuse to do: scoped keys,
every action logged, and a human approval step for anything
irreversible.
</p>

<h2>What I sell</h2>

<p>
One thing: a 14-day pilot that puts an agent on one of your
support or portal workflows. Triage plus guarded refunds within
your policy, or portal pulls with screenshot proof. $1,200 fixed,
and the second half is only due if it closes real work. After
that, $400 per month care, pause anytime.
One thing: a 14-day pilot that puts an agent on one of your support or
portal workflows. Triage plus guarded refunds within your policy, or
portal pulls with screenshot proof. $1,200 fixed, and the second half
is only due if it closes real work. After that, $400 per month care,
pause anytime.
</p>

<p>
<a href="/pilot">
Book the pilot here
</a>
. I reply within a day.
<a href="/pilot">Book the pilot here</a>. I reply within a day.
</p>

<h2>Why trust a solo builder</h2>
Expand All @@ -71,9 +68,9 @@ export default function AboutPage() {

<p>
The public case registry remains online as a reference: numbered,
tagged, searchable reports of agent incidents, with a moderation
queue and anonymous submissions. It no longer defines this site;
it is the evidence shelf the installs stand on.
tagged, searchable reports of agent incidents, with a moderation queue
and anonymous submissions. It no longer defines this site; it is the
evidence shelf the installs stand on.
</p>

<div id="contact">
Expand All @@ -90,8 +87,8 @@ export default function AboutPage() {
<div className="mt-10 border-t border-border-default pt-6">
<p className="font-mono text-xs text-text-tertiary">
AgentPostmortem is an independent project. We are not affiliated
with Anthropic, OpenAI, Google, Microsoft, or any AI vendor named
in the case database.
with Anthropic, OpenAI, Google, Microsoft, or any AI vendor named in
the case database.
</p>
</div>
</div>
Expand Down
15 changes: 9 additions & 6 deletions app/(public)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ export default function HomePage() {
</h1>

<p className="mt-6 max-w-[56ch] text-lg leading-relaxed text-stone-600">
Done for you install for support and ops teams. Triage plus
small refunds within policy, portal pulls with screenshot proof,
risky actions paused for your one tap approval.
Done for you install for support and ops teams. Triage plus small
refunds within policy, portal pulls with screenshot proof, risky
actions paused for your one tap approval.
</p>

<div className="mt-8 flex flex-col gap-3 sm:flex-row">
Expand Down Expand Up @@ -125,7 +125,10 @@ export default function HomePage() {
</header>

{/* Plan cards */}
<section aria-label="How the pilot works" className="grid gap-4 md:grid-cols-3">
<section
aria-label="How the pilot works"
className="grid gap-4 md:grid-cols-3"
>
{PLAN_CARDS.map((card, i) => (
<div
key={card.step}
Expand Down Expand Up @@ -213,8 +216,8 @@ export default function HomePage() {
If it does not close real work, you do not pay the second half.
</h2>
<p className="mx-auto mt-4 max-w-[52ch] text-[16px] leading-relaxed text-emerald-100/80">
$1,200 fixed for the 14 day pilot. Then $400 per month care,
pause anytime. Reply within a day.
$1,200 fixed for the 14 day pilot. Then $400 per month care, pause
anytime. Reply within a day.
</p>
<Link
href="/pilot"
Expand Down
35 changes: 27 additions & 8 deletions app/(public)/pilot/PilotForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,18 @@ function fieldClass(invalid: boolean): string {
export function PilotForm() {
const [values, setValues] = useState<Values>(EMPTY);
const [errors, setErrors] = useState<PilotErrors>({});
const [touched, setTouched] = useState<Partial<Record<PilotField, boolean>>>({});
const [touched, setTouched] = useState<Partial<Record<PilotField, boolean>>>(
{},
);
const [formError, setFormError] = useState("");
const [sending, setSending] = useState(false);
const [done, setDone] = useState("");
const refs = useRef<
Partial<
Record<PilotField, HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null>
Record<
PilotField,
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null
>
>
>({});

Expand All @@ -75,7 +80,13 @@ export function PilotForm() {
}

function focusFirstInvalid(next: PilotErrors) {
const order: PilotField[] = ["name", "email", "company", "track", "workflow"];
const order: PilotField[] = [
"name",
"email",
"company",
"track",
"workflow",
];
for (const field of order) {
if (next[field]) {
setTouched((prev) => ({ ...prev, [field]: true }));
Expand Down Expand Up @@ -152,7 +163,11 @@ export function PilotForm() {

const errorLine = (field: PilotField) =>
touched[field] && errors[field] ? (
<p id={`pilot-${field}-error`} role="alert" className="mt-1.5 text-[13px] font-medium text-sev-critical">
<p
id={`pilot-${field}-error`}
role="alert"
className="mt-1.5 text-[13px] font-medium text-sev-critical"
>
{errors[field]}
</p>
) : null;
Expand Down Expand Up @@ -208,7 +223,8 @@ export function PilotForm() {
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<div>
<label htmlFor="pilot-company" className={LABEL}>
Company or site <span className="font-normal text-text-tertiary">(optional)</span>
Company or site{" "}
<span className="font-normal text-text-tertiary">(optional)</span>
</label>
<input
id="pilot-company"
Expand Down Expand Up @@ -284,7 +300,10 @@ export function PilotForm() {
</div>

{/* Honeypot: invisible to humans, irresistible to bots. */}
<div aria-hidden="true" className="absolute h-px w-px overflow-hidden opacity-0">
<div
aria-hidden="true"
className="absolute h-px w-px overflow-hidden opacity-0"
>
<label htmlFor="pilot-website">Website</label>
<input
id="pilot-website"
Expand All @@ -310,8 +329,8 @@ export function PilotForm() {
</p>
)}
<p className="mt-3 text-[13px] leading-relaxed text-text-tertiary">
No payment now. Half up front only when we agree on the workflow,
second half only if it closes real work.
No payment now. Half up front only when we agree on the workflow, second
half only if it closes real work.
</p>
</form>
);
Expand Down
23 changes: 17 additions & 6 deletions app/(public)/pilot/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ export default function PilotPage() {
</span>
</h1>
<p className="mt-5 max-w-[56ch] text-lg leading-relaxed text-text-secondary">
$1,200 fixed. Second half only if it closes real work. Then $400
per month care, pause anytime.
$1,200 fixed. Second half only if it closes real work. Then $400 per
month care, pause anytime.
</p>
</header>

<div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,26rem)] lg:items-start">
<div>
<section aria-label="How it works" className="grid gap-4 sm:grid-cols-3">
<section
aria-label="How it works"
className="grid gap-4 sm:grid-cols-3"
>
{STEPS.map((s) => (
<div
key={s.n}
Expand All @@ -110,7 +113,10 @@ export default function PilotPage() {
</h2>
<ul className="mt-4 space-y-2.5">
{INCLUDED.map((item) => (
<li key={item} className="flex items-start gap-2.5 text-[15px] leading-relaxed text-text-secondary">
<li
key={item}
className="flex items-start gap-2.5 text-[15px] leading-relaxed text-text-secondary"
>
<span className="mt-0.5 grid h-5 w-5 shrink-0 place-items-center rounded-full bg-emerald-100 text-[12px] font-bold text-emerald-700">
</span>
Expand All @@ -130,13 +136,18 @@ export default function PilotPage() {
<summary className="cursor-pointer list-none font-sans text-[15px] font-bold text-text-primary marker:hidden [&::-webkit-details-marker]:hidden">
<span className="flex items-center justify-between gap-4">
{f.q}
<span className="text-emerald-700 transition-transform group-open:rotate-45">+</span>
<span className="text-emerald-700 transition-transform group-open:rotate-45">
+
</span>
</span>
</summary>
<p className="mt-2 text-[15px] leading-relaxed text-text-secondary">
{f.a}{" "}
{f.q.startsWith("Why") && (
<Link href="/tools" className="font-semibold text-emerald-700 hover:text-emerald-800">
<Link
href="/tools"
className="font-semibold text-emerald-700 hover:text-emerald-800"
>
See the proof.
</Link>
)}
Expand Down
19 changes: 6 additions & 13 deletions app/(public)/tools/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,9 @@ export default function ToolsPage() {
What the pilot runs on
</h1>
<p className="mt-3 max-w-2xl text-text-secondary">
These backends run the 14-day pilot behind the scenes. They are
not separate products to evaluate. Try any of them live, then{" "}
<a
href="/pilot"
className="text-accent hover:underline"
>
These backends run the 14-day pilot behind the scenes. They are not
separate products to evaluate. Try any of them live, then{" "}
<a href="/pilot" className="text-accent hover:underline">
book the pilot
</a>
.
Expand Down Expand Up @@ -285,13 +282,9 @@ export default function ToolsPage() {

<div className="mt-10 rounded-sm border border-accent/40 bg-accent-soft p-5">
<p className="text-sm leading-relaxed text-text-secondary">
One support or portal workflow, live in 14 days with guardrails
from day one. $1,200 fixed, second half only if it closes real
work.{" "}
<a
href="/pilot"
className="font-medium text-accent hover:underline"
>
One support or portal workflow, live in 14 days with guardrails from
day one. $1,200 fixed, second half only if it closes real work.{" "}
<a href="/pilot" className="font-medium text-accent hover:underline">
Book the pilot
</a>
.
Expand Down
23 changes: 17 additions & 6 deletions app/api/pilot-lead/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ const consumeSharedRateLimit = vi.fn(async () => ({
resetAt: new Date().toISOString(),
currentCount: 1,
}));
const send = vi.fn(async () => ({ data: { id: "mail-1" }, error: null }));
const send = vi.fn(async (_payload: Record<string, unknown>) => ({
data: { id: "mail-1" },
error: null,
}));
const logEvent = vi.fn();

vi.mock("@/lib/utils/hash", () => ({
Expand Down Expand Up @@ -40,7 +43,8 @@ const valid = {
email: "ada@company.com",
company: "company.com",
track: "support",
workflow: "Triage sixty Zendesk tickets every morning and refund damaged orders.",
workflow:
"Triage sixty Zendesk tickets every morning and refund damaged orders.",
};

describe("POST /api/pilot-lead", () => {
Expand All @@ -62,16 +66,21 @@ describe("POST /api/pilot-lead", () => {

expect(response.status).toBe(200);
expect(send).toHaveBeenCalledOnce();
const args = send.mock.calls[0][0] as Record<string, unknown>;
const args = send.mock.calls[0][0];
expect(args.to).toBe("hello@agentpostmortem.com");
expect(args.reply_to).toBe("ada@company.com");
expect(args.replyTo).toBe("ada@company.com");
expect(args.subject).toContain("Ada Lovelace");
});

it("returns every field error at once", async () => {
const { POST } = await import("./route");
const response = await POST(
createRequest({ name: "", email: "nope", track: "support", workflow: "short" }),
createRequest({
name: "",
email: "nope",
track: "support",
workflow: "short",
}),
);
const data = (await response.json()) as {
errors: Record<string, string>;
Expand All @@ -98,7 +107,9 @@ describe("POST /api/pilot-lead", () => {

it("answers honeypot submissions with fake success and sends nothing", async () => {
const { POST } = await import("./route");
const response = await POST(createRequest({ ...valid, website: "spam-bot" }));
const response = await POST(
createRequest({ ...valid, website: "spam-bot" }),
);
const data = (await response.json()) as { ok: boolean };

expect(response.status).toBe(200);
Expand Down
2 changes: 1 addition & 1 deletion app/api/pilot-lead/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export async function POST(req: NextRequest) {
const { error } = await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL ?? "noreply@agentpostmortem.com",
to: "hello@agentpostmortem.com",
reply_to: email,
replyTo: email,
subject: `Pilot application: ${name}`,
html: [
`<p><strong>Name:</strong> ${escapeHtml(name)}</p>`,
Expand Down
6 changes: 1 addition & 5 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@
rgba(5, 150, 105, 0.06),
transparent 62%
),
linear-gradient(
to bottom,
rgba(28, 25, 23, 0.028) 1px,
transparent 1px
);
linear-gradient(to bottom, rgba(28, 25, 23, 0.028) 1px, transparent 1px);
background-size:
100% 1400px,
100% 28px;
Expand Down
6 changes: 3 additions & 3 deletions components/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ export function Footer() {
</span>
</div>
<p className="max-w-[18rem] font-sans text-[13px] leading-relaxed text-text-tertiary">
Ops agents installed with guardrails, built on documented
failure evidence. Failure reports quoted on this site are
aggregated from public sources and may be unverified.
Ops agents installed with guardrails, built on documented failure
evidence. Failure reports quoted on this site are aggregated from
public sources and may be unverified.
</p>
</div>

Expand Down
8 changes: 6 additions & 2 deletions components/post/SubmitForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ function FieldLabel({

function FieldError({ message }: { message?: string }) {
if (!message) return null;
return <p className="mt-1 font-sans text-xs font-medium text-accent">{message}</p>;
return (
<p className="mt-1 font-sans text-xs font-medium text-accent">{message}</p>
);
}

const inputBase =
Expand Down Expand Up @@ -212,7 +214,9 @@ export function SubmitForm() {
<CheckIcon size={16} />
</span>
</div>
<h2 className="font-sans text-xl font-bold tracking-tight text-text-primary">Case Filed</h2>
<h2 className="font-sans text-xl font-bold tracking-tight text-text-primary">
Case Filed
</h2>
<p className="mt-2 text-sm text-text-secondary">
Your report is in the moderation queue. Once approved, it will be
assigned a permanent case number.
Expand Down
Loading