---
id: "5.6"
title: "Plays engine"
dept: engineering
status: draft
owner: Param
branded: false
last_verified: 2026-10-26
sources:
  - "brew origin/main: moves-engine.js:1-30, 473-492, 553-626, 652-674; data/move-specs.json (66 specs); prompts/move-house.md; sources/moves/ (51 files)"
  - "brew origin/main: build.js:58, 110-116, 356-430, 1068-1080, 1156, 1547-1600, 1745, 1777; rewrite-pot.js:1-60; refresh.js:1-6; moves-cadence.js:1-10, 274, 409, 501"
  - "brew origin/main: deploy.sh:39 (model and cap settings); tools/prod-env.sh:9"
  - "memory brew-lean-pipeline, brew-move-freshness, brew-connect-refresh, brew-pro-switch-and-flush, brew-gemini-provider, brew-model-compare-2026-10-20, keep-all-moves, brew-arata-reload-2026-10-20, brew-qa-round2-2026-10-24"
---

# Plays engine

## Answer

The Plays engine writes every applicable Play for a brand from its brief and ledger, checks each number and claim, turns each Play into cards as it lands, and keeps Plays current on a capped weekly budget; this page settles the specs, the order, the model, the caps and the rewrite rules.

## Today

In code, files, routes and ids a Play is still called a move. People see Plays. [src: memory brew-qa-round2-2026-10-24]

### Specs

- 66 specs in `data/move-specs.json`: 0 the index, 1 to 50 core, 51 to 65 listening-era (written once their evidence exists). Groups: reach 16, convert 12, know 11, found 11, keep 9, expand 6, index 1. [src: brew/data/move-specs.json]
- Each spec names its group, skill, kicker, discipline, title and stat hints, dependencies, evidence, lookups, an exemplar and a timing. [src: brew/data/move-specs.json]
- Dynamic Plays 100 to 999 (a festival at its plan point, a research gap, replies that reached a threshold) and weekly cadence Plays from 1001 (listening and automation, never reused). [src: brew/moves-engine.js:1-3] [src: brew/moves-cadence.js:9, 274]
- `status.total` is the applicable playbook (1 to 999, not the index, not cadence). [src: memory brew-lean-pipeline]

### Writing one Play

`writeMove(n)` = the house prompt + evidence contract + the discipline's skill file + brand brief + ledger slice (plus listening, pulse, video and calendar lines) + dependency digests + one Arata Play as depth exemplar. [src: brew/moves-engine.js:4-7]

| Setting | Value | Source |
|---|---|---|
| Model | `GEMINI_MOVES_MODEL` (`gemini-3.1-pro-preview`) on Gemini; Opus on Claude | [src: brew/moves-engine.js:17] [src: brew/deploy.sh:39] |
| Effort | `MOVES_EFFORT`, default medium; a max-tokens hit retries at low | [src: brew/build.js:398] [src: brew/moves-engine.js:473-479] |
| Cache | System and brand block cached for an hour | [src: brew/moves-engine.js:473] |
| Search | `MOVES_SEARCH`, default 1 | [src: brew/build.js:398] |
| Concurrency | `MOVES_CONCURRENCY` 8 on staging and production; a second pass at 4 for failures | [src: brew/deploy.sh:39] [src: brew/build.js:404-406] |
| Order | Wave A first: 1, 2, 3, 9, 10, 4, 5, 6, 7, 8; then dependency order | [src: brew/build.js:58] |

### Checking one Play

`verify()` = a deterministic number scan against the ledger, a fast-model claims check on the lines the Play rests on, one targeted fix. `verified` is true only when the claims check ran with no issues, no stat is unbacked, no number is unresolved and no ledger key is unknown. [src: brew/moves-engine.js:553-626]

Cover numbers are checked again at load (`stat-check.js`): an entry price that does not match the catalog, a "cited in reviews" figure not in a review, or an unbacked stat becomes "(to confirm)". [src: brew/docs/audit-2026-10-24-design-partners.md]

### As each Play lands

It is saved as `moves/NN.json` and in `moves.json`, turned into 1 or 2 cards, Play 21 starts the static ads, progress and pace are saved, and the team is emailed once wave A is done. PDFs are drawn when someone opens one (`pdfOn`), versioned by `PDF_V` 12. [src: brew/build.js:378-395, 1745, 1777]

### Caps

| Cap | Value | Source |
|---|---|---|
| Build per brand | `BUILD_CAP_USD` 45 | [src: brew/deploy.sh:39] [src: brew/tools/prod-env.sh:9] |
| All builds per day | `DAILY_BUILD_CAP_USD` 250 | same |
| Pro brand build | max(150, 2.5 × cap) | [src: brew/build.js:115] |
| Failed Plays | retried up to `BUILD_RETRY_MAX` 6 times, then the brand is Ready with them named | [src: brew/build.js:110, 418-421] |

### Keeping Plays current

Reading is cheap and frequent; rewriting is batched, needs a real change, and is capped. [src: memory brew-move-freshness]

| Trigger | What it does | Cap | Source |
|---|---|---|---|
| A fresh read or fact change | `refresh.js` finds material changes (price, product, rival, claim, policy, live ads, 10%+), a fast model judges which Plays change, flags them | | [src: brew/refresh.js:1-6] |
| Out of stock or removed product | flags every Play naming it, high priority, no AI | | [src: brew/build.js:1600-1640] |
| A rival's 10%+ price move or launch | rewrites the Play it hits hardest, at most one a day | | [src: brew/server.js:510-514] |
| First connected account | rewrites every Play privately after a 30-minute settle and the first read | `CONNECT_CAP_USD` 40 | [src: brew/build.js:1070-1072] [src: memory brew-connect-refresh] |
| Connector numbers move 10% from the base | flag, then the weekly run | `LIVE_CAP_USD` 30 a week | [src: brew/build.js:1076] |
| Learnings | high and medium impact now; low waits for the week | `LEARN_CAP_USD` 30 | [src: brew/build.js:1075] |
| Hourly stale sweep | queues flagged Plays within the weekly pot, best kind first; never Plays written in the last 24 hours | the pot | [src: brew/build.js:1570-1595] |

The weekly pot per brand is $10 on Flash, $40 on Pro and $50 on Claude; each kind may spend only down to its share (learnings 1.0, events 0.9, check 0.8, connected 0.7, low 0.6, new 0.5). [src: brew/rewrite-pot.js:14-21]

Rewrite priority in the queue: learnings, then events, manual and connected, then stale, weekly and live, then new. [src: brew/build.js:1156]

### Weekly cadence Plays

Listening Plays (2 to 4 from what was heard) and automation Plays (2 to 4 routines written as plans the team runs; Brew never runs them). One run per brand across instances by a 30-minute mark. [src: brew/moves-cadence.js:1-10, 501]

### Quality and cost

On 20 Oct, 54 Arata Plays compared: Gemini Pro median $0.18, 2.4 minutes, about 2,250 words; Opus median $0.83, 5.7 minutes, about 5,450 words with far more facts and sources. Opus read clearly better, but it also had the team's uploaded audit. [src: memory brew-model-compare-2026-10-20]

## Tomorrow

| Item | Version | Tag |
|---|---|---|
| Every applicable Play is kept; cost cuts make each Play cheaper, never fewer | v1 | Stays [src: memory keep-all-moves] |
| Plays on Gemini Pro, the rest on Flash | v1 | Changes once Param picks the launch model |
| "Verified" reached on most Plays | v2 | New (needs the claims check and fix to converge) |
| Dedupe of the same ask across Plays at write time (today only at serve time) | v2 | New |
| One weekly rewrite budget across every source | v1 | Stays (built as the pot) |

## How we do it

1. Never propose fewer Plays to save money; cut tokens, caching and repeat evidence calls. [src: memory keep-all-moves]
2. New flag sources only flag; the stale sweep and the weekly run rewrite. [src: memory brew-move-freshness]
3. Check a matcher against real brand data (variants) before deploying; one 15 ml variant once flagged 54 Arata Plays. [src: memory brew-move-freshness]
4. One-off rewrites use the ops switch, then confirm `meta.rewrittenAt` changed:

```bash
gcloud run services update shopos-feed --region=asia-south1 --update-env-vars=OPS_REWRITE=<slug>:<n,n>
```

[src: memory brew-qa-audit-2026-10-20]

5. Before a long paid run, check credit and expect other sessions to deploy; runs resume, but plan for it. [src: memory brew-arata-rewrite-2026-10-15]

## Numbers

| Metric | Definition | Where measured |
|---|---|---|
| Cost per Play | `meta.usd` × 1.5 overhead | `moves/NN.json` meta; `rewrite-pot.js` OVERHEAD [src: brew/rewrite-pot.js:24] |
| Build cost per brand | Total for a full build | `cost.json` |
| Verified share | Plays with `meta.verified` true | `moves.json` (Proposed to show on the ops page) |
| Stale Plays | Flagged and not yet rewritten | `updates.json` stale |

## Open

1. **Which model writes Plays at launch?** Proposal: Opus when Anthropic credit exists, since the 20 Oct comparison favoured it; run the fair 5-Play test first (about $1).
2. **Should an unverified Play show to the brand?** Proposal: yes, with "N claims to review" as today, never hidden.
