Files
PIG-Demo/.claude/workflows/new-environment.js
T
karti-ai 1a6bbf3309
ci / web (push) Failing after 1m14s
ci / python (push) Successful in 2m36s
new-environment workflow: accept an optional prior-art reference
2026-08-28 18:16:55 -07:00

185 lines
11 KiB
JavaScript

export const meta = {
name: 'new-environment',
description: 'Promote a vertical from published spec to a live environment: specification, Python env, TypeScript port, rollouts, demo page',
whenToUse: 'Adding an environment to demo.primeintellectgrowth.com. Pass { slug, vertical, seeds }.',
phases: [
{ title: 'Specify', detail: 'design the environment, then attack the design' },
{ title: 'Implement', detail: 'Python environment and its probe ladder' },
{ title: 'Port', detail: 'TypeScript engine and the demo module, in parallel' },
{ title: 'Capture', detail: 'real rollouts — serialised, spark-1 is single-stream' },
{ title: 'Verify', detail: 'adversarial review across four lenses' },
],
}
const REPO = '/home/kartios/repos/gitea/PIG-Demo'
const slug = (args && args.slug) || 'support-resolution'
const vertical = (args && args.vertical) || 'customer-support-resolution'
const pkg = slug.replace(/-/g, '_')
const seeds = (args && args.seeds) || '0-7'
const reference = (args && args.reference) || ''
const BASE = `You are building environment "${slug}" for the repo at ${REPO}, promoting the vertical "${vertical}" from a published specification to a live, playable demo at demo.primeintellectgrowth.com.
**Read \`.claude/skills/new-environment/SKILL.md\` first, in full.** It carries the judgement; this prompt carries only your stage. Also read \`CONTRACT.md\`, \`AGENTS.md\`, and the reference implementation in \`envs/wordle_five/\` and \`src/demos/wordle/\`.
The specification for this vertical — task, reward, counterweight, persona, anxiety — is already written and reviewed in \`src/content/verticals.ts\`. Start from that entry; do not invent a different task.
Package: \`envs/${pkg}/\`. Demo: \`src/demos/${slug}/\`. Traces: \`public/traces/${slug}/\`.
${reference ? `\n**Prior art to read before designing anything:** ${reference}\n` : ''}
Do NOT run git. Do NOT edit another stage's files.`
const SPEC_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['episode', 'data', 'actions', 'grader', 'components', 'ladder', 'heldOut', 'risks'],
properties: {
episode: { type: 'string' },
data: { type: 'string', description: 'Source, synthetic-or-real, licence' },
actions: { type: 'string' },
grader: { type: 'string', description: 'How the score is computed, deterministically' },
components: { type: 'array', items: { type: 'object', additionalProperties: false,
required: ['key','label','weight','role','definition'],
properties: { key:{type:'string'}, label:{type:'string'}, weight:{type:'number'},
role:{type:'string', enum:['objective','counterweight','gate']}, definition:{type:'string'} } } },
ladder: { type: 'array', items: { type: 'object', additionalProperties: false,
required: ['policy','behaviour','expected'],
properties: { policy:{type:'string'}, behaviour:{type:'string'}, expected:{type:'string'} } } },
heldOut: { type: 'string' },
risks: { type: 'array', items: { type: 'string' } },
},
}
phase('Specify')
const spec = await agent(`${BASE}
## YOUR STAGE: the specification. No code.
Design the environment concretely and write \`envs/${pkg}/SPEC.md\`. Answer the seven questions in the skill's stage 1 with specifics, not restatements of the vertical entry.
The two decisions that make or break this:
**The grader must compute, not opine.** If you find yourself reaching for an LLM judge, you have not found the environment yet. Look for the signal that already exists in the business: a downstream system's response, a settled number, a later observed outcome. If there genuinely is not one, say so plainly in \`risks\` — shipping it as a spec with the caveat stated is a legitimate outcome and better than faking it.
**The counterweight must be in genuine tension with the objective.** Name the crude policy that maxes the objective, and the component that stops it. Then name a good policy that pays the counterweight's price — if you cannot, the counterweight is a gate wearing the wrong label.
Also decide the data honestly. Synthetic is fine and often the only option; it must be labelled synthetic on the page, and it must not imply real customer records.
Return the structured spec AND write SPEC.md.`, { label: 'spec', phase: 'Specify', schema: SPEC_SCHEMA, effort: 'high' })
const specText = spec ? JSON.stringify(spec, null, 2) : '(the spec stage returned nothing)'
const LENSES = [
'DETERMINISM: is this grader really computable from the transcript, every time, with no human or model judgement anywhere in it? Hunt for a step that quietly needs an opinion.',
'GAMEABILITY: find the policy that maximises this reward while doing something the buyer would be appalled by. If you find one, the counterweight is insufficient. Try at least four distinct cheats.',
'HONESTY: would an executive be misled by this? Check the data provenance, whether the task is materially easier than the real job, and whether anything here implies a customer, a result, or a capability we have not got.',
]
const REVIEW_SCHEMA = {
type: 'object', additionalProperties: false,
required: ['verdict', 'findings'],
properties: {
verdict: { type: 'string', enum: ['sound', 'fixable', 'fatal'] },
findings: { type: 'array', items: { type: 'object', additionalProperties: false,
required: ['problem','fix'], properties: { problem:{type:'string'}, fix:{type:'string'} } } },
},
}
const reviews = (await parallel(LENSES.map((lens, i) => () =>
agent(`Attack this environment specification through ONE lens. Be adversarial: your job is to find the reason it should not ship, not to approve it.
LENS: ${lens}
${BASE}
=== THE SPECIFICATION ===
${specText}`, { label: `attack:${i}`, phase: 'Specify', schema: REVIEW_SCHEMA, effort: 'high' })
))).filter(Boolean)
const reviewText = reviews.map((r, i) => `--- LENS ${i} — verdict: ${r.verdict}\n${r.findings.map(f => ` ! ${f.problem}\n -> ${f.fix}`).join('\n')}`).join('\n\n')
if (reviews.some(r => r.verdict === 'fatal')) {
log('A reviewer called the specification fatal. Stopping before implementation.')
return { stopped: 'fatal specification', spec, reviews }
}
phase('Implement')
const python = await agent(`${BASE}
## YOUR STAGE: the Python environment.
Build \`envs/${pkg}/\` — engine, protocol, reward (with \`# region: pig-demo/reward\` markers), a reference policy, and tests. Register your probe policies in \`envs/probe.py\` for taskset "${slug}".
Implement the specification below, INCLUDING every fix the reviewers demanded. Where a reviewer found a gameable policy, add it to the probe ladder as a rung that must score below the good policies — a cheat you have named and measured is a cheat that cannot come back.
**Your gate:** \`uv run python envs/probe.py\` passes every assertion, including that your two good policies do not dominate each other. Iterate until it does. If it cannot pass, the reward is wrong — fix the reward, not the assertion.
=== SPECIFICATION ===
${specText}
=== WHAT THE REVIEWERS DEMANDED ===
${reviewText}`, { label: 'python', phase: 'Implement', effort: 'high' })
phase('Port')
const ported = (await parallel([
() => agent(`${BASE}
## YOUR STAGE: the TypeScript engine.
Port \`envs/${pkg}/\` 's scorer to \`src/demos/${slug}/engine.ts\`, and extend \`scripts/conformance.mjs\` so this environment's digest is gated the way wordle's is. Seed with FNV-1a over the decimal seed — never a built-in RNG, or the same seed picks a different task on each side and every permalink lies.
**Your gate:** \`pnpm conformance\` — the two digests match.
The Python that landed:
${python}`, { label: 'port:ts', phase: 'Port', effort: 'high' }),
() => agent(`${BASE}
## YOUR STAGE: the demo module.
Build \`src/demos/${slug}/\`\`meta.ts\`, \`narrative.ts\` (\`claims\` for all four tabs: play, watch, reward, evidence), \`surface.tsx\`, \`adapter.ts\`, \`reward.ts\`, and \`interactive\` if a human can play this task. Read \`src/demos/wordle/\` as the reference.
Build against \`src/components/demo/mock.tsx\` — do NOT wait for real rollouts, that is the whole point of the wire format.
Anything the demo needs that the shell does not offer is a contract bug to fix in \`demo-kit\` or expose as a \`SlotRegion\`, never a slug check in \`src/components/demo/\`.
**Your gate:** \`pnpm check\` and \`./node_modules/.bin/tsc --noEmit\` clean for your files.
The specification:
${specText}`, { label: 'port:demo', phase: 'Port', effort: 'high' }),
])).filter(Boolean)
phase('Capture')
const captured = await agent(`${BASE}
## YOUR STAGE: real rollouts.
Extend \`envs/capture.py\` for taskset "${slug}" and capture seeds ${seeds} for at least three arms: a weak one, a strong one, and a generated reference policy.
⚠️ **spark-1 serves one model and is single-stream — run the arms one after another, never concurrently.** Check it is up first: \`curl http://100.127.247.67:8001/v1/models\`. A thinking-enabled arm can take minutes per seed; run it in the background, redirect output to a FILE (piping to head/tail block-buffers Python's stdout and a working run looks hung), and poll the written fixtures rather than the log.
Include at least one run the agent loses. Then \`uv run python envs/build_manifest.py\`.
**Your gate:** \`uv run python envs/verify_fixtures.py\` — every fixture replays and reproduces its own recorded rewards at delta 0.
If spark-1 is unreachable, do NOT invent fixtures. Say so and stop; a fabricated rollout on this site would be worse than a missing one.`, { label: 'capture', phase: 'Capture', effort: 'high' })
phase('Verify')
const CHECKS = [
'Run the full gate chain and make it green: uv run python envs/probe.py, uv run pytest, uv run python envs/verify_fixtures.py, pnpm check, pnpm conformance, pnpm build. Fix whatever is broken.',
'Open the new demo in a real browser at 1280px and 390px (playwright is installed; serve dist). Confirm: it appears in the landing picker, all four tabs render, Play works if it has an interactive mode, ?tab= round-trips, zero console errors, no horizontal overflow at 390px.',
'Audit honesty against the rules in the skill: is synthetic data labelled synthetic ON THE PAGE, are recorded runs labelled recorded, does any claim imply a customer or a result we do not have, and does the page state how this environment differs from the real job? Fix the copy where it does not hold.',
]
const verified = (await parallel(CHECKS.map((c, i) => () =>
agent(`${BASE}\n\n## YOUR STAGE: verification.\n\n${c}\n\nReport precisely what you ran, what failed, and what you changed. Do not report success you did not observe.`,
{ label: `verify:${i}`, phase: 'Verify', effort: 'high' })
))).filter(Boolean)
return {
slug, vertical,
spec, reviews: reviewText,
python, ported, captured,
verified,
}