diff --git a/.agents/skills/done-done-verify-first-draft/SKILL.md b/.agents/skills/done-done-verify-first-draft/SKILL.md index c2d4803..6037787 100644 --- a/.agents/skills/done-done-verify-first-draft/SKILL.md +++ b/.agents/skills/done-done-verify-first-draft/SKILL.md @@ -51,6 +51,7 @@ Harness: A shell runs the CLI and curl for the url. A browser tool opens the url - [Logo mode: concepts from the brief (wordmark-led, abstract, symbol; distinct base shapes; letter + object dropped), self-critique at 16 and 32 px, brand sheets with kind, idea and palette, checked SVG files and the app icon set](features/logo-mode.md): Run `first-draft-axi logo "" --out tea.html --options 3` (Chrome and an image-capable model required); deterministic `test/logo.test.mjs`; nonpaid whole-viewer, concept, critique and icon-set capture of a real run via `FD_LOGO_SOURCE= done-done run logo-recapture` (`src/logo.mjs`, `src/logo-concepts.mjs`, `src/make.mjs`, `src/cli.mjs`, `journeys/logo.mjs`) - [Authenticated engines are required](features/ai-engine-required.md): Offline no-credential refusal through `test/engine-required.test.mjs` and independent make/judge CLI integrations through `test/critic.test.mjs`; one real normal-HOME `make --options 1 --first-view` must name distinct generator/judge engines and models. No fixture or self-assessment result counts as live proof. - [The independent critic and local reference library](features/make-critic.md): `npm test` retains one real-CLI make/independent-judge integration and one retained-output `judge` integration in `test/critic.test.mjs`; it no longer covers the former detailed capture, retry, and refusal matrix; nonpaid sealed report via `done-done run critic`; live retained assessment via `first-draft-axi judge "" --plain ` (`src/critic.mjs`, `src/human-side.mjs`, `src/judge.mjs`, `src/reference-library.mjs`, `src/reference-library.json`, `src/reference-capture.mjs`, `src/reference-network.mjs`, `src/cli.mjs`, `src/render-check.mjs`, `journeys/critic.mjs`) +- [Motion kept and judged: an animated option keeps its code, plays, and is scored on its frames](features/motion-kept-and-judged.md): Make from a brief that asks for motion and read the option html (inline ` diff --git a/docs/wrapper-allowlist.json b/docs/wrapper-allowlist.json index 6f84345..690811c 100644 --- a/docs/wrapper-allowlist.json +++ b/docs/wrapper-allowlist.json @@ -218,22 +218,31 @@ "reason": "An owner decision about honesty, not a design restriction (2026-10-24). It replaced the engine rule that labelled all sample content, which produced \"Placeholder quote\" and \"concept design\" copy (bake-off cause 2)." }, { - "id": "script-drawn-once", + "id": "script-kept-inline", "source": "contract", "kind": "delivery", - "what": "Script may run once; the host keeps what the first render shows, without the script.", - "sha256": "17b216e97598b4f5", + "what": "Inline script and CSS animation are kept and play; a script loaded from a URL runs once and the host keeps what that first render shows, without any script.", + "sha256": "09292f0980fef96e", "verdict": "accepted", - "reason": "Where Chrome is installed the host draws the page once into a static document; the sentence says what survives." + "reason": "Where Chrome is installed the exported page keeps its inline script under a policy that loads and sends nothing; a script from a URL cannot run there, so that page is drawn once into a static document. The sentence says what survives. It replaced script-drawn-once (2026-10-30), which dropped every script and with it the motion a brief asked for." }, { - "id": "no-script", + "id": "script-inline-only", "source": "contract", "kind": "delivery", - "what": "Write no script: the page is shown as a static document.", - "sha256": "63d1c2d956a51103", + "what": "Script is inline only: a script with a src or a javascript: URL is refused; inline script and CSS animation play.", + "sha256": "4ca6767afc035144", "verdict": "accepted", - "reason": "Without Chrome the page cannot be drawn, so script would not run in the exported document." + "reason": "Without Chrome the page cannot be drawn, so a script from a URL would not run in the exported document; inline script runs under the page's own policy, which loads and sends nothing. It replaced no-script (2026-10-30)." + }, + { + "id": "motion-for-real", + "source": "contract", + "kind": "policy", + "what": "Motion the brief asks for is written for real (CSS animation or inline script), not as a still, honouring prefers-reduced-motion.", + "sha256": "a8b9700872e8c2bc", + "verdict": "accepted", + "reason": "An owner decision (2026-10-30): a brief that asks for a terminal animation, a pulse or a marquee gets that motion in the option. Under no-script the model was told to remove exactly what was asked for, and the animation was hand-rolled in the product instead. It restricts nothing the plain call would do." }, { "id": "critic-flow-check", diff --git a/journeys/motion.mjs b/journeys/motion.mjs new file mode 100644 index 0000000..37f3ce1 --- /dev/null +++ b/journeys/motion.mjs @@ -0,0 +1,103 @@ +// Nonpaid real-CLI proof that an animated option keeps its motion: a brief that asks for a typing terminal -> make keeps the option's inline +// script -> the frame series at 390 and 1280 sits beside it -> the independent judge is asked about motion and returns a verdict -> the view +// plays the option in its sandboxed frame and lists the frames -> a still page gets no motion score -> one polish round with owner feedback +// still moves. Local stand-ins answer the model and the judge, so scores are synthetic, never design quality. +import '../test/fixtures/provider-hooks.mjs'; +import assert from 'node:assert/strict'; +import { execFile, spawn } from 'node:child_process'; +import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import { renderCheck } from '../src/render-check.mjs'; +import { PLAIN, standIn } from '../test/fixtures/critic-model.mjs'; +import { installJudge } from '../test/fixtures/judge-cli.mjs'; +const root = new URL('..', import.meta.url).pathname, out = process.env.DONE_DONE_EVIDENCE_ROOT ?? process.argv[2]; +if (!out) throw Error('Give an evidence folder'); +mkdirSync(out, { recursive: true }); +const BRIEF = 'A home screen for a developer tool, with a terminal that types out the install command (a typing animation)'; +// What the stand-in model writes: a terminal that types its lines in an inline script, with a cursor that blinks three times. +const terminal = (title) => `${title} +

${title}

Install first-draft in one line.

+`; +const dir = mkdtempSync(join(tmpdir(), 'fd-motion-')); +installJudge(dir); +let draws = 0; // each page the stand-in writes is its own, so a polish round is a changed option +const seen = [], server = await standIn(seen, [], false, () => {}, (title) => terminal(`${title}, draw ${++draws}`)), render = renderCheck(); +let view; +try { + const env = { ...process.env, PATH: `${dir}:${process.env.PATH}`, CI: 'true', HOME: dir, XDG_CONFIG_HOME: dir, XDG_DATA_HOME: dir, TMPDIR: dir, FIRST_DRAFT_HOME: dir, OPENCODE_API_KEY: 'synthetic', FIRST_DRAFT_TEST_BYOKIT_BASE: `http://127.0.0.1:${server.address().port}`, FD_POLISH_SCORES: '[7,8]' }; + const run = async (...args) => JSON.parse((await promisify(execFile)(process.execPath, [join(root, 'src/cli.mjs'), ...args], { env, cwd: dir, maxBuffer: 64_000_000 })).stdout.trim().split('\n').at(-1)); + await run('init', '--agent', 'byokit:opencode-go', '--model', 'muse-spark-1.3-contributor'); + + // 1. The animated brief in: the option keeps its script under a policy that runs inline script and loads nothing. + const { made } = await run('make', BRIEF, '--out', join(dir, 'terminal.html'), '--options', '1'); + const [file] = made.files, html = readFileSync(file.file, 'utf8'); + assert.match(html, /