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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ First public release candidate.
- Responsive standalone React Email renderer with compliance placeholders.
- Explicit `punch generate` CLI and composable TypeScript API.
- Atomic new-directory output with optional redacted structured traces.
- Sonnet 5 plain-JSON compatibility with adaptive thinking explicitly disabled.
- Live-tested provider deadlines of 120 seconds per call and 360 seconds per
campaign.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ the useful generative part while making commerce facts inspectable:
- public fetching rejects local/private networks, unsafe redirects, oversized
responses and credential-bearing URLs.

## Live showcase

<p align="center">
<img src="https://raw.githubusercontent.com/plmn95/punch/main/docs/showcase/northstar-campaign.png" alt="A live Punch campaign for the fictional Northstar Goods brand" width="700" />
</p>

This campaign was generated live with Claude Sonnet 5 from two public, newly
fictional product pages. The `sales` safety policy was combined with a custom
desk-reset brief; both supplied products remained grounded and all ten campaign
and render checks passed.

[Inspect the brief, source commit and validation record](https://github.com/plmn95/punch/blob/main/docs/showcase/README.md).

## Quick start

Punch currently ships from source. Node.js 24 or newer and an Anthropic API key
Expand Down Expand Up @@ -78,6 +91,37 @@ Add `--trace` for redacted structured stage artifacts or `--json` for exactly
one terminal JSON result on stdout. Run `node dist/cli/bin.js --help` for the
complete explicit interface.

## Custom campaign briefs

Punch has three fixed goal policies and open-ended campaign direction. Keep the
factual rules of `sales`, `product-launch` or `promotion`, then add a bounded
brief with `--instructions`:

```bash
node dist/cli/bin.js generate \
--website "https://example.com" \
--product "https://example.com/products/first-product" \
--goal "sales" \
--instructions "Create a concise gift guide for first-time buyers." \
--output "./campaign"
```

Useful briefs include a buying angle, intended reader, hierarchy or tone. They
cannot authorise invented facts, unknown products, unsupported offers or a
different goal policy. In other words: three safety modes, unlimited campaign
briefs.

The same seam is available through the TypeScript API:

```ts
const giftGuide = {
goal: "sales" as const,
instructions: "Create a gift guide organised by recipient.",
};

await generateCampaign({ website, products, ...giftGuide }, { provider });
```

## TypeScript API

```ts
Expand Down
49 changes: 49 additions & 0 deletions docs/showcase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Live Northstar Goods showcase

![Generated Northstar Goods campaign](northstar-campaign.png)

This is a real Punch generation from public HTTP input through Claude Sonnet 5,
deterministic validation, React Email rendering and atomic output. Northstar
Goods, both products, their copy and their artwork are newly fictional material
created for this repository.

## Brief

```text
Goal policy: sales
Custom instructions: Create a concise desk-reset gift guide for people building
calmer daily routines. Feature both products equally.
```

The source pages are pinned at commit
[`ac59faf`](https://github.com/plmn95/punch/tree/ac59faf96b47b9ee6fafee6f360cb5c03c59d38f/docs/showcase/source).
For the live canary, their exact HTML was served through a public test endpoint;
the resulting long test URLs are intentionally omitted from the committed
showcase.

## Result

- Subject: “A calmer desk, two useful things at a time”
- Blocks: header, hero, two-column product grid and closing CTA
- Products: Orbit Catchall at 34.00 GBP and Daybreak Pouch at 28.00 GBP
- Validation: all ten grounding, claim and render checks passed
- Generation: emit and critique completed without a revision
- Secrets, raw pages and raw provider responses: absent from artifacts

The compact machine-readable record is in [`result.json`](result.json). Raw
campaign and trace artifacts are deliberately not committed because the test
endpoint encodes the fictional HTML in its URLs; they remain less readable than
the pinned source and reviewed result.

## What the canary caught

Claude Sonnet 5 enables adaptive thinking by default. Punch requires one strict
plain-text JSON response, so its adapter now explicitly disables thinking and
continues to reject every non-text response block. The live run also showed that
the earlier 60-second call deadline was too short for the current model; bounded
defaults are now 120 seconds per provider call and 360 seconds for the complete
campaign. Both remain configurable through the TypeScript API.

See Anthropic's
[Sonnet 5 migration guide](https://platform.claude.com/docs/en/models/sonnet-5/migration-guide)
for the model's thinking behaviour.
Binary file added docs/showcase/northstar-campaign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions docs/showcase/result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"showcaseSchemaVersion": "0.1.0",
"generatedOn": "2026-08-28",
"model": "claude-sonnet-5",
"sourceCommit": "ac59faf96b47b9ee6fafee6f360cb5c03c59d38f",
"goal": "sales",
"instructions": "Create a concise desk-reset gift guide for people building calmer daily routines. Feature both products equally.",
"subject": "A calmer desk, two useful things at a time",
"preheader": "Meet the Orbit Catchall and Daybreak Pouch from Northstar Goods.",
"blockTypes": [
"header-standard",
"hero-stacked",
"product-grid",
"cta-block"
],
"products": [
{
"productId": "product-01",
"name": "Orbit Catchall",
"amount": "34.00",
"currency": "GBP"
},
{
"productId": "product-02",
"name": "Daybreak Pouch",
"amount": "28.00",
"currency": "GBP"
}
],
"validation": {
"valid": true,
"checks": [
"campaign-grounding",
"campaign-claims",
"render-contrast",
"render-font-floor",
"render-cta-height",
"render-html-bytes",
"render-grid-geometry",
"render-compliance",
"render-resource-urls",
"render-product-binding"
]
},
"generation": {
"calls": ["emit:primary", "critique:primary"],
"inputTokens": 76354,
"outputTokens": 7990,
"revised": false
},
"capture": {
"path": "northstar-campaign.png",
"width": 700,
"height": 1052,
"sha256": "a7e63ea6dc897fc3c4fff290844d487a965bf36d798cde7e554341c629d0d34c"
}
}
8 changes: 8 additions & 0 deletions docs/showcase/source/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Northstar Goods source fixture

Northstar Goods and its products are newly fictional material created for the
Punch live showcase. The HTML intentionally provides simple, explicit brand and
commerce evidence; it is not a production storefront.

The showcase generation uses an immutable public copy of these files so the
input can be inspected alongside Punch's validated output.
11 changes: 11 additions & 0 deletions docs/showcase/source/assets/daybreak-pouch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/showcase/source/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/showcase/source/assets/orbit-catchall.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions docs/showcase/source/daybreak-pouch.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="Daybreak Pouch" />
<meta
property="og:description"
content="A compact zip pouch with two inner pockets for chargers, pens and the small things that usually disappear."
/>
<meta property="og:image" content="assets/daybreak-pouch.svg" />
<meta property="product:price:amount" content="28.00" />
<meta property="product:price:currency" content="GBP" />
<meta property="product:availability" content="in stock" />
<title>Daybreak Pouch — Northstar Goods</title>
</head>
<body>
<main>
<h1>Daybreak Pouch</h1>
<img
src="assets/daybreak-pouch.svg"
alt="Sky blue and coral Daybreak Pouch"
/>
<p>£28.00</p>
<p>In stock.</p>
<p>
A compact zip pouch with two inner pockets for chargers, pens and the
small things that usually disappear.
</p>
</main>
</body>
</html>
52 changes: 52 additions & 0 deletions docs/showcase/source/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:site_name" content="Northstar Goods" />
<meta
name="description"
content="Quietly colourful desk and travel goods for everyday routines."
/>
<title>Northstar Goods</title>
<style>
:root {
--ink: #17223b;
--paper: #fffaf1;
--coral: #f46b5d;
--sky: #78c8d3;
}

body {
max-width: 760px;
margin: 64px auto;
padding: 0 24px;
color: var(--ink);
background: var(--paper);
font:
18px/1.6 system-ui,
sans-serif;
}

.logo {
width: 220px;
}

a {
color: var(--coral);
}
</style>
</head>
<body>
<img class="logo" src="assets/logo.svg" alt="Northstar Goods logo" />
<h1>Useful things, with a little lift.</h1>
<p>
Northstar Goods makes quietly colourful desk and travel companions for
everyday routines. Clear shapes, warm colour and no unnecessary fuss.
</p>
<nav>
<a href="orbit-catchall.html">Orbit Catchall</a>
<a href="daybreak-pouch.html">Daybreak Pouch</a>
</nav>
</body>
</html>
32 changes: 32 additions & 0 deletions docs/showcase/source/orbit-catchall.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="Orbit Catchall" />
<meta
property="og:description"
content="A rounded aluminium tray that keeps keys, cables and small desk essentials in one bright landing spot."
/>
<meta property="og:image" content="assets/orbit-catchall.svg" />
<meta property="product:price:amount" content="34.00" />
<meta property="product:price:currency" content="GBP" />
<meta property="product:availability" content="in stock" />
<title>Orbit Catchall — Northstar Goods</title>
</head>
<body>
<main>
<h1>Orbit Catchall</h1>
<img
src="assets/orbit-catchall.svg"
alt="Coral and navy Orbit Catchall tray"
/>
<p>£34.00</p>
<p>In stock.</p>
<p>
A rounded aluminium tray that keeps keys, cables and small desk
essentials in one bright landing spot.
</p>
</main>
</body>
</html>
4 changes: 2 additions & 2 deletions src/generation/run-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ import {
} from "./stage-invariants.js";
import { callStructured } from "./structured-call.js";

const DEFAULT_CALL_TIMEOUT_MS = 60_000;
const DEFAULT_RUN_TIMEOUT_MS = 180_000;
const DEFAULT_CALL_TIMEOUT_MS = 120_000;
const DEFAULT_RUN_TIMEOUT_MS = 360_000;

/** Configured generation dependencies and engine-owned deadline bounds. */
export type GenerationEngineOptions = Readonly<{
Expand Down
1 change: 1 addition & 0 deletions src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function createTextModel(client: Anthropic, modelId: string): TextModel {
{
model: modelId,
max_tokens: request.maxOutputTokens,
thinking: { type: "disabled" },
system: request.system,
messages: [{ role: "user", content: request.user }],
},
Expand Down
Loading