diff --git a/skill/agents/impeccable-asset-producer.md b/skill/agents/impeccable-asset-producer.md index ac2101ed6..95afd1d79 100644 --- a/skill/agents/impeccable-asset-producer.md +++ b/skill/agents/impeccable-asset-producer.md @@ -24,7 +24,7 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh ## Decision Sketch Batches -When the parent hands you a decision payload instead of an approved mock, the job is one sketch per card, the canon card included, written to each card's declared `sketch` path while the decision page waits. Work from each card's structured fields and PRODUCT.md alone; the card is the packet, and a card too thin to brief a sketch is reported back, not padded from imagination. Render the whole set through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across the set. Uniformity outranks per-card polish; a sketch that looks more finished than its siblings has broken the comparison the page exists to hold. Write each file the moment it is done so the page reveals sketches as they land, and return a one-line manifest per sketch instead of the full production manifest. +When the parent hands you a decision payload instead of an approved mock, the job is one sketch per card, the canon card included, written to each card's declared `sketch` path while the decision page waits. Work from each card's structured fields and PRODUCT.md alone; the card is the packet, and a card too thin to brief a sketch is reported back, not padded from imagination. Render the whole set through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across the set. The only legible text is the product's real name and one real headline per sketch; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Uniformity outranks per-card polish; a sketch that looks more finished than its siblings has broken the comparison the page exists to hold. Produce in the parent's reading order, the assigned card first, then the hand, then canon, and write each file the moment it is done so the page reveals sketches as they land; the first file is also your heartbeat, so never let planning delay it. Return a one-line manifest per sketch instead of the full production manifest. ## Input Contract diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index dbb86f705..69bb9ff2a 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -44,11 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. 4. Run `node {{scripts_path}}/concept-seed.mjs --scope direction --mode ` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer every dealt challenger as a named alternate, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it, and dropping a challenger from the presentation takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node {{scripts_path}}/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. When the harness runs subagents, spawn the shipped asset producer with the payload file and PRODUCT.md, one pass for the whole set; otherwise generate inline after serving. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents, spawn the shipped asset producer with the payload file and PRODUCT.md, one pass for the whole set; otherwise generate inline after serving. Waiting on the choice is supervision, not idleness: every `--wait` pass that returns 3 is a checkpoint on the sketch directory, and when no file has landed within two minutes of the spawn, interrupt the producer and produce the remaining sketches inline, because a stalled opaque batch reads to the user as a dead page. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index f93e37ec6..555219aba 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -222,14 +222,25 @@ if (hasFlag('start')) { const key = arg('key') || Math.random().toString(16).slice(2, 10); // In start mode the agent is alive and owns browser routing; the server // only opens the system browser itself when --open forces it. + // The daemon's output lands in a per-key log so a startup failure can say + // what actually went wrong instead of only that it did. + const logFile = path.join(QUESTION_DIR, `${key}.log`); + const logFd = fs.openSync(logFile, 'a'); const child = spawn(process.execPath, [ fileURLToPath(import.meta.url), '--payload', payloadPath, '--detached-serve', '--key', key, '--timeout', String(timeoutSec), ...(hasFlag('open') ? [] : ['--no-open']), - ], { detached: true, stdio: 'ignore' }); + ], { detached: true, stdio: ['ignore', logFd, logFd] }); child.unref(); + fs.closeSync(logFd); const deadline = Date.now() + 8000; while (Date.now() < deadline && !fs.existsSync(stateFile(key))) await new Promise((r) => setTimeout(r, 100)); - if (!fs.existsSync(stateFile(key))) { console.error('serve-question: server failed to start'); process.exit(1); } + if (!fs.existsSync(stateFile(key))) { + let tail = ''; + try { tail = fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-4).join('\n '); } catch { /* log never written */ } + console.error(`serve-question: server failed to start${tail ? `\n ${tail}` : ''}`); + console.error(`serve-question: log at ${path.relative(process.cwd(), logFile) || logFile}. A sandboxed exec that cannot listen on localhost causes exactly this; rerun this command once through the harness's network-enabled or unsandboxed command tool before falling back.`); + process.exit(1); + } const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8')); console.log(`QUESTION URL: ${state.url}`); console.log(`QUESTION KEY: ${key}`); @@ -617,13 +628,16 @@ function page() { document.querySelectorAll('.media.sketching').forEach(m => { const url = m.dataset.sketch; const img = m.querySelector('img.sketch'); + const note = m.querySelector('.sketch-note'); const started = Date.now(); - const settle = () => { m.classList.remove('sketching'); m.querySelector('.shimmer')?.remove(); }; + // A live elapsed count is the difference between "working" and "frozen". + const tick = setInterval(() => { if (note) note.textContent = 'sketching ยท ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const settle = () => { clearInterval(tick); m.classList.remove('sketching'); m.querySelector('.shimmer')?.remove(); }; const tryLoad = () => { const probe = new Image(); probe.onload = () => { img.src = probe.src; img.hidden = false; settle(); }; probe.onerror = () => { - if (Date.now() - started > 300000) { + if (Date.now() - started > 150000) { const pip = m.querySelector('.pip img'); if (pip) { img.src = pip.getAttribute('src'); img.hidden = false; } else { m.closest('.face').classList.add('text-only'); m.remove(); }