diff --git a/CHANGELOG.md b/CHANGELOG.md index a9834ab00..232fda0d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **A check that did not run now says so, and memory stops losing transcripts.** **Codex and the other non-Claude hosts find gstack in every step of every skill.** -This release closes the severe bugs left in the tracker. Two promises lead. First, nothing is silently lost: memory ingest marks a transcript saved only after gbrain confirms it landed, regenerated design variants never overwrite the ones you paid for, and learnings that could not be written say so. Second, no false all-clear: a Codex review whose sandbox could not start, a retired Codex model, a design binary killed at launch, and a learnings search with no Bun all used to read as "ready", "pass" or "no results". Each now prints `not run` or `unavailable` with the reason and the fix command, and every one of those messages has a section in the new [docs/troubleshooting.md](docs/troubleshooting.md). +This release closes the severe bugs left in the tracker. Two promises lead. First, nothing is silently lost: memory ingest marks a transcript saved only after gbrain confirms it landed, and learnings that could not be written say so. Second, no false all-clear: a Codex review whose sandbox could not start, a retired Codex model, a design binary killed at launch, and a learnings search with no Bun all used to read as "ready", "pass" or "no results". Each now prints `not run` or `unavailable` with the reason and the fix command, and every one of those messages has a section in the new [docs/troubleshooting.md](docs/troubleshooting.md). ### The numbers that matter @@ -78,7 +78,7 @@ The first two rows are the ones to care about: a review that read nothing, or fo - **Memory and gbrain.** Ingest stamped pages gbrain skipped or never stored (#2778); `--no-write` wrote state. A project's `.env` `DATABASE_URL` reached a local PGLite brain (#1931). Offline or off-VPN looked like a broken config, and dot-leading project names (`.claude`) blocked ingest forever (#2884). Transcripts from every repository shared one source (#2232). `/sync-gbrain` said "pushed" when the artifacts source had 0 indexed pages (#2670), forced the full dream cycle (#2783), always reported the call graph as unknown, and kept deleted worktrees' sources silently. The 1.27 artifacts-repo rename never worked, and installs it repointed at a repo that never existed are repaired (#1437). gbrain install and source wiring needed `jq` (#2369). - **Codex.** A retired model (HTTP 404) or a custom provider with a wrong `base_url` reported `ready` (#2843). Custom providers configured with `env_key` failed the auth check (#2192). Large prompts failed on argv limits (Windows 32 KB, Linux E2BIG) and the macOS fallback timeout dropped the prompt (#1674, #1686). - **Non-Claude hosts.** Later skill blocks lost `$GSTACK_*`, `$B` and `$D` and ran `/bin/gstack-...` (#1159). Codex skipped the symlinked router. design/dist and make-pdf/dist were missing from runtime roots (#2891). /ship and /plan-ceo-review overflowed the 160 KB host limit. The OpenCode `/gstack-review` command loaded nothing (#2651). `/browse` disappeared after toggling `skill_prefix` (#2263). Section files linked to checkout copies instead of rendered ones (#2707). skillify pointed at Claude's install on every host. /gstack-upgrade could run git cleanup in your own project when its path variable was unset. -- **Design.** Regenerating variants overwrote earlier rounds (#1737). A design binary killed at launch reported `DESIGN_READY`. /design-consultation and /design-shotgun never loaded your taste profile. Plan reviews read a root DESIGN.md as the feature's design doc (#2840). +- **Design.** A design binary killed at launch reported `DESIGN_READY`. /design-consultation and /design-shotgun never loaded your taste profile. Plan reviews read a root DESIGN.md as the feature's design doc (#2840). - **/ship, /review and daily tools.** /ship stopped in versionless repos and monorepos (#2334, #2343). The plan audit picked an unrelated plan (#2797). Worktrees split project state (#2792). Learnings search exited 0 without Bun (#2794) and ranked poorly (#2796, #2799). Logs ignored `$TMPDIR` in sandboxed shells (#2952). /plan-tune said "calibrated" with no signals. /setup-deploy printed part of the Render API key (#1096). /analytics durations read 0s (#1728). Rejected taste preferences used the wrong confidence bucket (#1777). /context-save duration was empty on Linux (#2704). /retro missed pytest, minitest, XCTest, Terraform and Bats tests and counted build output (#2812, #2809, #2037). Plan reviews now treat design docs and handoff notes as data (#2818). - **Browser.** Browse failed behind `HTTP_PROXY` (#494). Headless browse and make-pdf ignored `GSTACK_CHROMIUM_PATH` (#2771, #1968, #1633); extensions opened a window (#2281). `prettyscreenshot --hide ... out.png` wrote elsewhere (#1419). `gstack-ios-qa-daemon --help` started the daemon (#1932). Aside in `~/.local/bin` was not found (#2965). - **Windows.** /freeze, /guard and /investigate denied every edit and their hooks failed under cmd.exe (#2876, #2354). Generators crashed with EEXIST (#2329). The node-server bundle inlined sharp and socks (#2260), and a minimal install needed `browse/src/server.ts` (#2439). The /cso helper failed on the current SDK and was reported as "install Visual Studio" (#3015). `gstack-config gbrain-refresh` misread status (#1981). @@ -99,7 +99,47 @@ The first two rows are the ones to care about: a review that read nothing, or fo - The weekly eval report counts only cases that ran: tests deselected for another tier or case shard are listed by reason with no credit, every result is attributed to its case (0 unattributed, was 72), and no PR-lane eval shard runs over 10 minutes (the deploy eval is split per case). - Codex evals pass only when Codex completed a command; a review whose sandbox failed now fails instead of passing. The AUQ matrix checks the `(recommended)` marker on option labels, where the auto-decide hook reads it. -Contributed by @CermakM (#2843), @Haijie-Lee (#2192), @SomSamantray (#2965), @Vaughan-g-aus (#2329), @BharadwajMittapelli (#2729), @maxpetrusenkoagent (#1931, #1932, #1981), @Infiniteyieldai (#2884), @afshaker (#2778), @time-attack (#2232, #2229), @jzeisweiss (#2670), @liutiming (#2783), @agile-operators (#1437), @benjaminberes-bp (#2369), @lewispeel (#2885), @Yugz29 (#2899), @fcmerle (#2962), @cdecook23 (#2784), @mb1810 (#2913), @ntdatt812 (#2949), @pneumorea-ai (#2915), @gabrielrondon (#2811), @kevingrasso32 (#2876), @xwang4-svg (#2354), @DizzyWesterwald (#2936), @oli548 (#2771), @shofel (#1968), @ryanlaiwy (#1633), @yinanli1917-cloud (#2281), @Spooks444 (#2260), @punksterlabs (#2439), @himiaocc (#2492), @jbetala7 (#1419, #1777), @mamedov (#2891), @Lockyer228 (#2651), @topcoder1 (#2263), @jonahberg (#2707), @AlinValentin7 (#3015), @philhie (#2208), @Alchemist-DevAI (#2334), @merit-blake (#2343), @mvanhorn (#1737), @day-of-davon (#2952), @loulanyue (#2797, #2792, #2794, #2796), @Bit0ps (#2840), @szsunyuan (#2799), @frosimanuel (#2818), @JiayuuWang (#1096), @RyanAlberts (#1728), @exGeni (#2704), @Jino00 (#2812), @snig-17 (#2809) and @SholtoMc (#2037). +Contributed by @CermakM (#2843), @Haijie-Lee (#2192), @SomSamantray (#2965), @Vaughan-g-aus (#2329), @BharadwajMittapelli (#2729), @maxpetrusenkoagent (#1931, #1932, #1981), @Infiniteyieldai (#2884), @afshaker (#2778), @time-attack (#2232, #2229), @jzeisweiss (#2670), @liutiming (#2783), @agile-operators (#1437), @benjaminberes-bp (#2369), @lewispeel (#2885), @Yugz29 (#2899), @fcmerle (#2962), @cdecook23 (#2784), @mb1810 (#2913), @ntdatt812 (#2949), @pneumorea-ai (#2915), @gabrielrondon (#2811), @kevingrasso32 (#2876), @xwang4-svg (#2354), @DizzyWesterwald (#2936), @oli548 (#2771), @shofel (#1968), @ryanlaiwy (#1633), @yinanli1917-cloud (#2281), @Spooks444 (#2260), @punksterlabs (#2439), @himiaocc (#2492), @jbetala7 (#1419, #1777), @mamedov (#2891), @Lockyer228 (#2651), @topcoder1 (#2263), @jonahberg (#2707), @AlinValentin7 (#3015), @philhie (#2208), @Alchemist-DevAI (#2334), @merit-blake (#2343), @day-of-davon (#2952), @loulanyue (#2797, #2792, #2794, #2796), @Bit0ps (#2840), @szsunyuan (#2799), @frosimanuel (#2818), @JiayuuWang (#1096), @RyanAlberts (#1728), @exGeni (#2704), @Jino00 (#2812), @snig-17 (#2809) and @SholtoMc (#2037). + +## [1.91.17.0] - 2026-10-03 + +**Checkpoints say whether each next step was run, read or guessed, and `/context-restore` verifies the guesses first.** +**Paid design images are never overwritten, and design skills always show the round you just paid for.** + +This wave rebuilds three community PRs from @mvanhorn on current main. A checkpoint's Remaining Work used to read the same whether a step had been run or only inferred, so a resumed session could act on a guess and hit the wrong system (#3004, reported by @tomg65). Running `$D variants` twice into one folder replaced `variant-A.png`, `$D generate --retry` kept only the last image, and skills built boards from fixed `variant-A/B/C` names, so you could pay for images you never saw (#1529, reported by @SakenW). + +### What changes for you + +- **Remaining Work carries provenance.** Every item starts with `Open.` and ends with how the saving session knows it: `(path run)` with the outcome, `(path read)` or `(path assumed)` for commands, flags, config values and files; `(target state checked)`, `(code read)` or `(path assumed)` for writes. For example: + + 1. Open. Run the migration check with SWITCH_B=1 against staging. (path assumed) + 2. Open. Run bun test test/billing.test.ts. (path run) exit 0 + + `/context-restore` keeps the saved order but shows item 2 under **Next steps** and item 1 under **Verify first**, and "continue" starts by verifying item 1 read-only instead of jumping to item 2. Checkpoints saved before this release have no markers, so their command, path and write items land under Verify first, with a one-line banner saying so. +- **Nothing paid is lost.** A taken image name gets `-2`, `-3` and so on (`variant-A-2.png`); stderr says `note: exists; saved to (existing file kept)` and the JSON reports the real path. Every `--retry` attempt is its own saved image. If saving fails after the API returned the image, the bytes go to a private recovery copy in your temp directory and the message says where; a local write failure never buys a second image. +- **Every image command prints JSON, even on failure**: `requested`, `saved`, `selected`, `failures`, `recovered`. Exit 0 means the result is ready, 2 means nothing was saved, 3 means the run stopped after saving some images. +- **Boards show this round only.** Skills tell you how many of the requested images were saved, write the round's printed paths to `board-images.json`, build the board with `$D compare --images-file`, archive an old `feedback.json` first, and record `approved_path` in `approved.json`, so a later session opens exactly the image you approved. Old letter-only approvals still work. /design-shotgun subagents stage in a fresh temp directory and publish with `gstack-design-claim`, which never overwrites. +- **If you script `$D`,** read paths from the JSON instead of assuming the file name: + + out=$($D generate --brief "pricing page" --output designs/pricing.png); rc=$? + img=$(printf '%s' "$out" | jq -r .outputPath) + [ "$rc" -eq 0 ] && $D check --image "$img" --brief "pricing page" + + `variants` prints its list in `.paths`; `compare` takes it losslessly with `--images-file paths.json`. +- **Windows console flashes (#1784)** were confirmed fixed on main (v1.91.15.0) by @kaiwulff on a real Windows 11 desktop; this release ships no code for them. + +### Itemized changes + +#### Fixed +- `/context-save` Remaining Work did not record whether a step was run, read or assumed, and `/context-restore` offered the first item as the next action (#3004). Marker format from #3019 by @mvanhorn, field-tested by @tomg65. +- Design images were silently overwritten by later rounds and `--retry` attempts (#1529). Exclusive-create claim idea from #1737 by @mvanhorn. +- Comparison boards and quality checks read fixed `variant-A/B/C` names or a `variant-*.png` glob and mixed rounds; a stale `feedback.json` could approve a new round. +- `$D` wrote iterate session files to `/tmp`, which does not exist on Windows, so `generate` could not record its session and `iterate` could not find it there; sessions now live in the OS temp directory. +- /setup-deploy reported a failing status command as success (the pipe hid its exit code), matched `cd` anywhere in a workflow as a deploy workflow, and left the merge-method check, Fly `/health` detection and GitHub-Actions-only status command unspecified. + +#### Added +- `$D compare --images-file` and JSON-array `--images`, `bin/gstack-design-claim`, `bin/gstack-design-approved`, and `approved_path` in approval records. +- Periodic behavior eval `context-restore-provenance-order` (3-trial panel) and free contract tests for the provenance markers, `$D` persistence and the design-skill path rules. ## [1.91.16.0] - 2026-10-03 diff --git a/TODOS.md b/TODOS.md index 24e7f2be5..c3891e9cf 100644 --- a/TODOS.md +++ b/TODOS.md @@ -164,6 +164,31 @@ Deferred from the approved severe fix wave (docs/designs/SEVERE_FIX_WAVE_2026_10 trials per case, apply the CASE_QUARANTINE entry rule instead of chasing one run at a time. Effort S. +### P2/P3: mvanhorn fix-wave deferrals (filed 2026-10-03, from the autoplan review of the wave) + +- **E6 (P2): behavior E2E for design round accounting** — stub `$D` so a + generating step reports `saved` shorter than `requested` (and exit 2 for zero + saved), run /plan-design-review or /design-shotgun against it, and assert the + skill tells the user how many paid images were saved, names the failures and + builds no board on zero saved. `test/design-printed-paths.test.ts` only pins + the marker order; whether the model reports the count is behavior. Context: + #1529. Effort M. +- **E7 (P3): prune or archive unapproved design rounds** — never-overwrite plus + `--retry` attempts and iterate outputs grow + `$GSTACK_STATE_ROOT/projects/$SLUG/designs/` without bound. Add a prune or + archive command for unapproved rounds and attempts (keep anything an + `approved.json` names) and show the design directory size in `$D gallery`. + Context: #1529, eng review. Effort S. +- **E5 (P3): restore verifies assumed items read-only** — `/context-restore` + could check `(path assumed)` and `(code read)` items itself with read-only + commands (file exists, flag is defined, table has a unique key) and report + what it found before offering option A. Context: #3004. Effort M. +- **E4 (P3): real-desktop Windows console smoke for spawn changes** — #1784 was + confirmed fixed on a real Windows 11 desktop by its reporter (v1.91.15.0); CI + can only check that spawns pass `windowsHide`. A smoke that starts browse from + a console-less parent and watches for new visible windows would catch a + regression the static sweep can't. Effort M. + ### P2/P3: parallel fix-wave follow-ups (filed 2026-10-02, from the approved fix-wave plan) - **Agent Skills distribution (#113)** — publish gstack in the Agent Skills diff --git a/bin/gstack-design-approved b/bin/gstack-design-approved new file mode 100755 index 000000000..7c3f360a2 --- /dev/null +++ b/bin/gstack-design-approved @@ -0,0 +1,24 @@ +#!/usr/bin/env bun +// gstack-design-approved — print the absolute path of the image an +// approved.json names. +// +// Usage: gstack-design-approved +// +// Prefers approved_path (relative to approved.json's directory). Legacy +// records with only approved_variant resolve to variant-.png. When the +// named image is missing, exits 1 with "approved image is missing; +// reselect from the board" and never substitutes another image. + +import { resolveApprovedImage } from '../design/src/approval'; + +const file = process.argv[2]; +if (!file) { + console.error('Usage: gstack-design-approved '); + process.exit(1); +} +const resolved = resolveApprovedImage(file); +if (!resolved.ok) { + console.error(resolved.error); + process.exit(1); +} +console.log(resolved.path); diff --git a/bin/gstack-design-claim b/bin/gstack-design-claim new file mode 100755 index 000000000..88a8c9f1f --- /dev/null +++ b/bin/gstack-design-claim @@ -0,0 +1,55 @@ +#!/bin/sh +# gstack-design-claim — publish a staged design image without overwriting. +# +# Usage: gstack-design-claim +# +# Creates with an exclusive, non-overwriting create (set -C +# noclobber, which opens with O_EXCL). When the name is taken it tries +# -2, -3 ... -999, the same rule as +# $D's claimOutputPath (x-2.png bumps to x-2-2.png). Prints the final path. +# On any failure it exits non-zero with problem/cause/fix and leaves the staged +# image in place, so the paid bytes survive. On success the staged copy is +# removed. + +set -u + +fail() { + printf 'gstack-design-claim: %s\ncause: %s\nfix: %s\n' "$1" "$2" "$3" >&2 + exit 1 +} + +[ "$#" -eq 2 ] || fail "expected 2 arguments, got $#" "usage: gstack-design-claim " "pass the staged image path, then the destination path" +src=$1 +dest=$2 + +[ -f "$src" ] && [ -s "$src" ] || fail "staged image $src is missing or empty" "the generating command did not save an image there" "use the outputPath the \$D command printed" + +dir=$(dirname "$dest") +base=$(basename "$dest") +case "$base" in + ?*.*) stem=${base%.*}; ext=".${base##*.}" ;; + *) stem=$base; ext="" ;; +esac + +err=$(mkdir -p "$dir" 2>&1) || fail "cannot create $dir; staged image kept at $src" "$err" "make $dir writable or choose another destination" + +n=1 +candidate=$dest +while :; do + if err=$( (set -C; : > "$candidate") 2>&1 ); then + if err=$(cat "$src" 2>&1 > "$candidate"); then + rm -f "$src" + printf '%s\n' "$candidate" + exit 0 + fi + rm -f "$candidate" + fail "cannot write $candidate; staged image kept at $src" "$err" "free disk space or make $dir writable, then rerun" + fi + if [ -e "$candidate" ] || [ -L "$candidate" ]; then + n=$((n + 1)) + [ "$n" -le 999 ] || fail "$dest and every name through $stem-999$ext already exist; staged image kept at $src" "999 images already use this name" "choose a new destination name or move older images out of $dir" + candidate="$dir/$stem-$n$ext" + continue + fi + fail "cannot create $candidate; staged image kept at $src" "$err" "make $dir writable or choose another destination" +done diff --git a/context-restore/SKILL.md b/context-restore/SKILL.md index f552fe9e1..d0345843b 100644 --- a/context-restore/SKILL.md +++ b/context-restore/SKILL.md @@ -506,6 +506,22 @@ checkpoint(s) from another project; only this project's are listed." If it print `LEGACY_BUCKET`, relay that line. If the chosen file is `ROOT_DIFFERS`, add "Saved from another checkout of this repository at `{project_root}`." as info. +**Sort Remaining Work by provenance.** Keep every item's original text and saved +order, and drop nothing. Put an item under **Verify first** when it: +- ends in `(path assumed)` or `(code read)`; +- ends in `(path run)` but its text reports a failure; +- has no marker and is a writing step (migration, sync, insert, import, a dialog that + writes) or names a concrete path (a runnable command, CLI flag or switch, config key + or value, or file or directory path). + +Every other item goes under **Next steps**: `(path run)` with a successful outcome, +`(path read)`, `(target state checked)`, and unmarked items that neither write nor +name a concrete path. Verifying means read-only inspection: read the file or the +target, or run a command that changes nothing. Checkpoints saved before provenance +markers existed have none, so their concrete-path items land under Verify first. When +the file has no provenance markers at all, print this line above the groups: +`This checkpoint predates provenance markers; items naming commands, paths or writes are listed under Verify first.` + Read the chosen file and present a summary: ``` @@ -522,7 +538,13 @@ Status: {status} {summary from saved file} ### Remaining Work -{remaining work items} +{legacy banner line, if it applies} + +Next steps +{Next steps items, in saved order, original text} + +Verify first (inspect read-only before executing anything) +{Verify first items, in saved order, original text} ### Notes {notes} @@ -540,7 +562,10 @@ After presenting, ask via AskUserQuestion: - B) Show the full saved file - C) Just needed the context, thanks -If A, summarize the first remaining work item and suggest starting there. +If A, take the first Remaining Work item in saved order. If it is under Next steps, +suggest starting there. If it is under Verify first, suggest verifying it (read-only) +before doing it or any later item, so a later runnable step never jumps ahead of an +unverified earlier one. --- diff --git a/context-restore/SKILL.md.tmpl b/context-restore/SKILL.md.tmpl index 4ba1571c9..55db95b21 100644 --- a/context-restore/SKILL.md.tmpl +++ b/context-restore/SKILL.md.tmpl @@ -180,6 +180,22 @@ checkpoint(s) from another project; only this project's are listed." If it print `LEGACY_BUCKET`, relay that line. If the chosen file is `ROOT_DIFFERS`, add "Saved from another checkout of this repository at `{project_root}`." as info. +**Sort Remaining Work by provenance.** Keep every item's original text and saved +order, and drop nothing. Put an item under **Verify first** when it: +- ends in `(path assumed)` or `(code read)`; +- ends in `(path run)` but its text reports a failure; +- has no marker and is a writing step (migration, sync, insert, import, a dialog that + writes) or names a concrete path (a runnable command, CLI flag or switch, config key + or value, or file or directory path). + +Every other item goes under **Next steps**: `(path run)` with a successful outcome, +`(path read)`, `(target state checked)`, and unmarked items that neither write nor +name a concrete path. Verifying means read-only inspection: read the file or the +target, or run a command that changes nothing. Checkpoints saved before provenance +markers existed have none, so their concrete-path items land under Verify first. When +the file has no provenance markers at all, print this line above the groups: +`This checkpoint predates provenance markers; items naming commands, paths or writes are listed under Verify first.` + Read the chosen file and present a summary: ``` @@ -196,7 +212,13 @@ Status: {status} {summary from saved file} ### Remaining Work -{remaining work items} +{legacy banner line, if it applies} + +Next steps +{Next steps items, in saved order, original text} + +Verify first (inspect read-only before executing anything) +{Verify first items, in saved order, original text} ### Notes {notes} @@ -214,7 +236,10 @@ After presenting, ask via AskUserQuestion: - B) Show the full saved file - C) Just needed the context, thanks -If A, summarize the first remaining work item and suggest starting there. +If A, take the first Remaining Work item in saved order. If it is under Next steps, +suggest starting there. If it is under Verify first, suggest verifying it (read-only) +before doing it or any later item, so a later runnable step never jumps ahead of an +unverified earlier one. --- diff --git a/context-save/SKILL.md b/context-save/SKILL.md index 72cfba419..f2b25b884 100644 --- a/context-save/SKILL.md +++ b/context-save/SKILL.md @@ -404,10 +404,30 @@ Using the gathered state plus your conversation history, produce a summary cover 1. **What's being worked on** — the high-level goal or feature 2. **Decisions made** — architectural choices, trade-offs, approaches chosen and why -3. **Remaining work** — concrete next steps, in priority order +3. **Remaining work** — concrete next steps, in priority order, each marked with + how this session knows it (see the provenance table below) 4. **Notes** — anything a future session needs to know (gotchas, blocked items, open questions, things that were tried and didn't work) +**Remaining Work provenance.** Every item starts with the status `Open.` and never +with a completion token such as `[done]` or `[executed]`. Then pick its trailing +marker by the first rule that applies: + +| # | The item is... | Marker | Use when this session... | +|---|----------------|--------|--------------------------| +| 1 | a writing step (migration, sync, insert, import, a dialog that writes), with or without a concrete path | `(target state checked)` | inspected the target's schema or state (for example the unique key) | +| | | `(code read)` | read the writer but not the target | +| | | `(path assumed)` | inferred the write without reading either | +| 2 | otherwise, names a concrete path: a runnable command, CLI flag or switch, config key or value, or file or directory path | `(path run)` | executed it and observed the outcome; state the outcome in the item (`exit 0`, `failed: `) | +| | | `(path read)` | read the code or file but did not execute it | +| | | `(path assumed)` | inferred it without running or reading it, however confident | +| 3 | anything else | none | (`Open.` alone) | + +Writing steps have their own markers because whether a write is safe to repeat +depends on the target's state, not on the code that writes. Examples: +`Open. Run the suite with CONFIG=staging. (path run) exit 0` and +`Open. Switch B reads config Y. (path assumed)`. + If the user provided a title, use it. Otherwise, infer a concise title (3-6 words) from the work being done. @@ -499,7 +519,7 @@ files_modified: ### Remaining Work -{Numbered list of concrete next steps, in priority order} +{Numbered list of concrete next steps, in priority order. Each item starts with `Open.` and ends with its provenance marker from Step 2, or no marker under rule 3.} ### Notes diff --git a/context-save/SKILL.md.tmpl b/context-save/SKILL.md.tmpl index d34aa3c22..d8313809e 100644 --- a/context-save/SKILL.md.tmpl +++ b/context-save/SKILL.md.tmpl @@ -79,10 +79,30 @@ Using the gathered state plus your conversation history, produce a summary cover 1. **What's being worked on** — the high-level goal or feature 2. **Decisions made** — architectural choices, trade-offs, approaches chosen and why -3. **Remaining work** — concrete next steps, in priority order +3. **Remaining work** — concrete next steps, in priority order, each marked with + how this session knows it (see the provenance table below) 4. **Notes** — anything a future session needs to know (gotchas, blocked items, open questions, things that were tried and didn't work) +**Remaining Work provenance.** Every item starts with the status `Open.` and never +with a completion token such as `[done]` or `[executed]`. Then pick its trailing +marker by the first rule that applies: + +| # | The item is... | Marker | Use when this session... | +|---|----------------|--------|--------------------------| +| 1 | a writing step (migration, sync, insert, import, a dialog that writes), with or without a concrete path | `(target state checked)` | inspected the target's schema or state (for example the unique key) | +| | | `(code read)` | read the writer but not the target | +| | | `(path assumed)` | inferred the write without reading either | +| 2 | otherwise, names a concrete path: a runnable command, CLI flag or switch, config key or value, or file or directory path | `(path run)` | executed it and observed the outcome; state the outcome in the item (`exit 0`, `failed: `) | +| | | `(path read)` | read the code or file but did not execute it | +| | | `(path assumed)` | inferred it without running or reading it, however confident | +| 3 | anything else | none | (`Open.` alone) | + +Writing steps have their own markers because whether a write is safe to repeat +depends on the target's state, not on the code that writes. Examples: +`Open. Run the suite with CONFIG=staging. (path run) exit 0` and +`Open. Switch B reads config Y. (path assumed)`. + If the user provided a title, use it. Otherwise, infer a concise title (3-6 words) from the work being done. @@ -173,7 +193,7 @@ files_modified: ### Remaining Work -{Numbered list of concrete next steps, in priority order} +{Numbered list of concrete next steps, in priority order. Each item starts with `Open.` and ends with its provenance marker from Step 2, or no marker under rule 3.} ### Notes diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index 8b1903df7..97716578d 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -561,12 +561,14 @@ For interactive feedback, use `compare --serve` and its printed HTTP URL; openin If `DESIGN_READY`: the design binary is available for visual mockup generation. Commands: -- `$D generate --brief "..." --output /path.png` — generate a single mockup -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants -- `$D compare --images "a.png,b.png,c.png" --output /path/board.html --serve` — comparison board + HTTP server +- `$D generate --brief "..." --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D compare --images-file /path/board-images.json --output /path/board.html --serve` — comparison board + HTTP server - `$D serve --html /path/board.html` — serve comparison board and collect feedback via HTTP - `$D check --image /path.png --brief "..."` — vision quality gate - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate + +Image commands never overwrite (a taken name gets `-2`) and always print JSON (`requested`, `saved`, `failures`); exit 0 ready, 2 nothing saved, 3 stopped after saving some. Capture without `set -e`: `_OUT=$($D ...); _RC=$?`. - `$D extract --image /absolute/path.png` — print tokens and automatically update DESIGN.md in the current Git repository; no read-only flag `generate` returns `sessionFile`; `iterate` requires that existing session. `variants` returns `paths` but creates no session: regenerate with an updated brief instead. @@ -676,7 +678,7 @@ fonts [top-3], colors [top-3], layouts [top-3], aesthetics [top-3]. Bias generation toward these unless the user explicitly requests a different direction. Also avoid their strong rejections: [top-3 rejected per dimension]." -**Legacy fallback:** Glob `$GSTACK_STATE_ROOT/projects/$SLUG/designs/**/approved.json` (resolve the root with gstack-paths); Read the five newest. Use explicit feedback only, never infer fonts/colors from variant letters. No usable files: continue without a taste profile. +**Legacy fallback:** Glob `$GSTACK_STATE_ROOT/projects/$SLUG/designs/**/approved.json` (resolve the root with gstack-paths); Read the five newest. To view an approved image, resolve it with `~/.claude/skills/gstack/bin/gstack-design-approved `. Use explicit feedback only, never infer fonts/colors from variant letters. No usable files: continue without a taste profile. **Conflict handling:** If the current user request contradicts a strong persistent signal (e.g., "make it playful" when taste profile strongly prefers minimal), flag diff --git a/design-consultation/sections/proposal-and-preview.md b/design-consultation/sections/proposal-and-preview.md index 8c9b95628..fa1546e25 100644 --- a/design-consultation/sections/proposal-and-preview.md +++ b/design-consultation/sections/proposal-and-preview.md @@ -306,15 +306,17 @@ echo "DESIGN_DIR: $_DESIGN_DIR" Brief: Phase 3 aesthetic/colors/type/spacing/layout plus Phase 1 product context: ```bash -$D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/" +_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` -`variants` writes into a new `round-/` under the output dir and prints `roundDir`, -`manifest` and `paths` (saved images only). Run the quality check on each of those -`paths`; never include failed variants: + +**Round accounting:** names are never overwritten (a taken one is bumped), so use only the printed `saved` paths. Tell the user how many of `requested` paid images were saved and name each `failures` entry. Exit 2 means nothing was saved: report `failures` and stop here (offer Path B or skip); run no `$D check` or board. + +Run quality check on each saved path, starting with the first; never include failed variants: ```bash -$D check --image "" --brief "" +$D check --image "" --brief "" ``` Read JSON, not exit code: `pass: false` means regenerate addressing `issues`, then recheck. `pass: true` with an unavailable/skipped warning is missing automated coverage; disclose it and inspect visually. @@ -325,10 +327,12 @@ Read each accepted PNG inline, then open the board with those paths before invit ### Comparison Board + Feedback Loop -Use the successful, quality-checked paths in this example: + +Write this round's board images (printed paths that passed checks, in order) as a JSON array to `$_DESIGN_DIR/board-images.json` with the Write tool; board letters A, B, C follow that order. Then archive any earlier Submit so it cannot approve these images, and build the board: ```bash -$D compare --images "" --output "$_DESIGN_DIR/design-board.html" --serve +[ -f "${_DESIGN_DIR:?set _DESIGN_DIR to the design dir printed above}/feedback.json" ] && mv "${_DESIGN_DIR:?}/feedback.json" "${_DESIGN_DIR:?}/feedback-$(date -u +%Y%m%dT%H%M%SZ).json" +$D compare --images-file "$_DESIGN_DIR/board-images.json" --output "$_DESIGN_DIR/design-board.html" --serve ``` This publishes to a persistent daemon, opens the board and exits. Read captured stderr for the startup marker; a PID is not readiness. Exit 0 with `BOARD_URL` means the daemon is serving. Save its full `http://127.0.0.1:N/boards//` URL. Only legacy `--no-daemon` needs a host background task; `SERVE_STARTED: port=N` gives root URL `http://127.0.0.1:N/`. @@ -350,18 +354,18 @@ After the response, read current feedback next to the board HTML: **Board or chat:** revisions regenerate; a final choice needs summary confirmation; skip goes to Phase 6 without a mockup. Ask if no choice/detail; never infer approval from a missing file. Submit with revision notes is a revision. **Regenerate:** -1. Revise the brief, preserving unrelated constraints. Archive this round's feedback files so old Submit cannot approve new images. -2. Run `$D variants` with the new brief (no session). Re-run the quality check and visual self-gate on every new image. -3. Rebuild: `$D compare --images "" --output "$_DESIGN_DIR/design-board.html"`, without `--serve`. +1. Revise the brief, preserving unrelated constraints. Archive this round's feedback files so old Submit cannot approve new images (the board block does this on rebuild). +2. Run `$D variants` with the new brief (no session), with the same capture and round accounting. Re-run the quality check and visual self-gate on every new image (its printed path). +3. Rebuild with the board block above (it rewrites board-images.json), without `--serve`. 4. Reload at the saved URL (keep its per-board path; legacy uses root): `jq -nc --arg html "$_DESIGN_DIR/design-board.html" '{html: $html}' | curl -sS -X POST "${BOARD_URL}api/reload" -H 'Content-Type: application/json' --data-binary @-` 5. Check reload succeeded, then AskUserQuestion at the same URL until a final choice, skip or stop. Failed generation/reload uses the fallback, not another wait. **SERVER FALLBACK:** Nonzero exit or no readiness marker: show each variant inline with Read, then AskUserQuestion: "The comparison board server failed to start. Which variant? Any changes?" Route chat feedback as above. -**After receiving feedback (any path):** summarize PREFERRED, RATINGS, YOUR NOTES, DIRECTION; AskUserQuestion "Is this right?" A confirmed final choice permits Write of `$_DESIGN_DIR/approved.json` with `approved_variant`, `approved_path` (absolute PNG), `round`, `feedback`, `date` (UTC), `screen` (the product page depicted by the chosen mockup), and `branch` (the current `git branch --show-current` result, empty if detached). Use valid JSON, never shell interpolation. This approves the image only; Q-final gates project writes. +**After receiving feedback (any path):** summarize PREFERRED, RATINGS, YOUR NOTES, DIRECTION; AskUserQuestion "Is this right?" A confirmed final choice permits Write of `$_DESIGN_DIR/approved.json` with `approved_variant`, `approved_path` (file name of that letter's entry in this board's `board-images.json`, never the directory listing), `feedback`, `date` (UTC), `screen` (the product page depicted by the chosen mockup), and `branch` (the current `git branch --show-current` result, empty if detached). Use valid JSON, never shell interpolation. This approves the image only; Q-final gates project writes. -After final image confirmation, `$D extract` would write DESIGN.md in a Git repo: run it only in a fresh non-repository scratch directory. Bind `$D` and `APPROVED_IMAGE` to absolute paths: +After final image confirmation, `$D extract` would write DESIGN.md in a Git repo: run it only in a fresh non-repository scratch directory. Bind `$D` and `APPROVED_IMAGE` (the confirmed letter's `board-images.json` entry) to absolute paths: ```bash _EXTRACT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gstack-design-extract-XXXXXXXX") || exit 1 @@ -377,7 +381,7 @@ _EXTRACT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gstack-design-extract-XXXXXXXX") || ex Compare extracted tokens with the approved image and verified fonts; show discrepancies at Q-final. Empty arrays, an "Unable to extract" mood or command failure → disclose fallback to Phase 3 values, never invent measured tokens. -Late visual changes return to the feedback loop: regenerate, recheck, reconfirm, then extract again. Only `generate` supplies `sessionFile` for `$D iterate --session "" --feedback "" --output "$_DESIGN_DIR/refined.png"`; variants must regenerate. +Late visual changes return to the feedback loop: regenerate, recheck, reconfirm, then extract again. Only `generate` supplies `sessionFile` for `$D iterate --session "" --feedback "" --output "$_DESIGN_DIR/refined.png"`; use its printed `outputPath`. Variants must regenerate. **Plan mode:** Carry the approved mockup paths/tokens into Phase 6's "## Proposed DESIGN.md" plan section. Its Q-final approval governs saving that content; defer the actual DESIGN.md to implementation. diff --git a/design-consultation/sections/proposal-and-preview.md.tmpl b/design-consultation/sections/proposal-and-preview.md.tmpl index 72cfab820..9328f6b44 100644 --- a/design-consultation/sections/proposal-and-preview.md.tmpl +++ b/design-consultation/sections/proposal-and-preview.md.tmpl @@ -111,15 +111,17 @@ echo "DESIGN_DIR: $_DESIGN_DIR" Brief: Phase 3 aesthetic/colors/type/spacing/layout plus Phase 1 product context: ```bash -$D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/" +_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` -`variants` writes into a new `round-/` under the output dir and prints `roundDir`, -`manifest` and `paths` (saved images only). Run the quality check on each of those -`paths`; never include failed variants: + +**Round accounting:** names are never overwritten (a taken one is bumped), so use only the printed `saved` paths. Tell the user how many of `requested` paid images were saved and name each `failures` entry. Exit 2 means nothing was saved: report `failures` and stop here (offer Path B or skip); run no `$D check` or board. + +Run quality check on each saved path, starting with the first; never include failed variants: ```bash -$D check --image "" --brief "" +$D check --image "" --brief "" ``` Read JSON, not exit code: `pass: false` means regenerate addressing `issues`, then recheck. `pass: true` with an unavailable/skipped warning is missing automated coverage; disclose it and inspect visually. @@ -130,7 +132,7 @@ Read each accepted PNG inline, then open the board with those paths before invit {{DESIGN_SHOTGUN_LOOP}} -After final image confirmation, `$D extract` would write DESIGN.md in a Git repo: run it only in a fresh non-repository scratch directory. Bind `$D` and `APPROVED_IMAGE` to absolute paths: +After final image confirmation, `$D extract` would write DESIGN.md in a Git repo: run it only in a fresh non-repository scratch directory. Bind `$D` and `APPROVED_IMAGE` (the confirmed letter's `board-images.json` entry) to absolute paths: ```bash _EXTRACT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gstack-design-extract-XXXXXXXX") || exit 1 @@ -146,7 +148,7 @@ _EXTRACT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gstack-design-extract-XXXXXXXX") || ex Compare extracted tokens with the approved image and verified fonts; show discrepancies at Q-final. Empty arrays, an "Unable to extract" mood or command failure → disclose fallback to Phase 3 values, never invent measured tokens. -Late visual changes return to the feedback loop: regenerate, recheck, reconfirm, then extract again. Only `generate` supplies `sessionFile` for `$D iterate --session "" --feedback "" --output "$_DESIGN_DIR/refined.png"`; variants must regenerate. +Late visual changes return to the feedback loop: regenerate, recheck, reconfirm, then extract again. Only `generate` supplies `sessionFile` for `$D iterate --session "" --feedback "" --output "$_DESIGN_DIR/refined.png"`; use its printed `outputPath`. Variants must regenerate. **Plan mode:** Carry the approved mockup paths/tokens into Phase 6's "## Proposed DESIGN.md" plan section. Its Q-final approval governs saving that content; defer the actual DESIGN.md to implementation. diff --git a/design-html/SKILL.md b/design-html/SKILL.md index fc410c736..d361c5539 100644 --- a/design-html/SKILL.md +++ b/design-html/SKILL.md @@ -409,13 +409,15 @@ Comparison boards are local HTML files: open them with `open file://...` on macO If `DESIGN_READY`: the design binary is available for visual mockup generation. Commands: -- `$D generate --brief "..." --output /path.png` — generate a single mockup -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants -- `$D compare --images "a.png,b.png,c.png" --output /path/board.html --serve` — comparison board + HTTP server +- `$D generate --brief "..." --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D compare --images-file /path/board-images.json --output /path/board.html --serve` — comparison board + HTTP server - `$D serve --html /path/board.html` — serve comparison board and collect feedback via HTTP - `$D check --image /path.png --brief "..."` — vision quality gate - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate +Image commands never overwrite (a taken name gets `-2`) and always print JSON (`requested`, `saved`, `failures`); exit 0 ready, 2 nothing saved, 3 stopped after saving some. Capture without `set -e`: `_OUT=$($D ...); _RC=$?`. + **Path rule:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. @@ -454,7 +456,7 @@ GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ setopt +o nomatch 2>/dev/null || true _APPROVED=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/approved.json 2>/dev/null | head -1) [ -n "$_APPROVED" ] && echo "APPROVED: $_APPROVED" || echo "NO_APPROVED" -_VARIANTS=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/variant-*.png "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/round-*/variant-*.png 2>/dev/null | head -1) +_VARIANTS=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/variant-*.png 2>/dev/null | head -1) [ -n "$_VARIANTS" ] && echo "VARIANTS: $_VARIANTS" || echo "NO_VARIANTS" _FINALIZED=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/finalized.html 2>/dev/null | head -1) [ -n "$_FINALIZED" ] && echo "FINALIZED: $_FINALIZED" || echo "NO_FINALIZED" @@ -465,11 +467,17 @@ Now route based on what was found. Check these cases in order: ### Case A: approved.json exists (design-shotgun ran) -If `APPROVED` was found, read it. Extract: approved variant PNG path, user feedback, -screen name. The PNG path is `approved_path` when present (it may sit in a `round-/` -directory); older files name only a letter in `approved_variant`, meaning -`variant-.png` beside approved.json. If that file is missing, say so and ask -which variant to use instead of picking the newest PNG. Also read the CEO plan if one exists (it adds strategic context). +If `APPROVED` was found, read it for user feedback and screen name, and resolve the +approved PNG (never guess it from a letter or the newest variant): + +```bash +~/.claude/skills/gstack/bin/gstack-design-approved "" +``` + +It prints the approved image's absolute path. If it prints "approved image ... is +missing; reselect from the board" instead, tell the user exactly that, offer +/design-shotgun to reselect, and do not substitute another image. Also read the CEO +plan if one exists (it adds strategic context). Read `DESIGN.md` if it exists in the repo root. These tokens take priority for system-level values (fonts, brand colors, spacing scale). diff --git a/design-html/SKILL.md.tmpl b/design-html/SKILL.md.tmpl index cc486a896..9fea4b7d0 100644 --- a/design-html/SKILL.md.tmpl +++ b/design-html/SKILL.md.tmpl @@ -71,7 +71,7 @@ GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ setopt +o nomatch 2>/dev/null || true _APPROVED=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/approved.json 2>/dev/null | head -1) [ -n "$_APPROVED" ] && echo "APPROVED: $_APPROVED" || echo "NO_APPROVED" -_VARIANTS=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/variant-*.png "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/round-*/variant-*.png 2>/dev/null | head -1) +_VARIANTS=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/variant-*.png 2>/dev/null | head -1) [ -n "$_VARIANTS" ] && echo "VARIANTS: $_VARIANTS" || echo "NO_VARIANTS" _FINALIZED=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/finalized.html 2>/dev/null | head -1) [ -n "$_FINALIZED" ] && echo "FINALIZED: $_FINALIZED" || echo "NO_FINALIZED" @@ -82,11 +82,17 @@ Now route based on what was found. Check these cases in order: ### Case A: approved.json exists (design-shotgun ran) -If `APPROVED` was found, read it. Extract: approved variant PNG path, user feedback, -screen name. The PNG path is `approved_path` when present (it may sit in a `round-/` -directory); older files name only a letter in `approved_variant`, meaning -`variant-.png` beside approved.json. If that file is missing, say so and ask -which variant to use instead of picking the newest PNG. Also read the CEO plan if one exists (it adds strategic context). +If `APPROVED` was found, read it for user feedback and screen name, and resolve the +approved PNG (never guess it from a letter or the newest variant): + +```bash +{{BIN_DIR}}/gstack-design-approved "" +``` + +It prints the approved image's absolute path. If it prints "approved image ... is +missing; reselect from the board" instead, tell the user exactly that, offer +/design-shotgun to reselect, and do not substitute another image. Also read the CEO +plan if one exists (it adds strategic context). Read `DESIGN.md` if it exists in the repo root. These tokens take priority for system-level values (fonts, brand colors, spacing scale). diff --git a/design-review/SKILL.md b/design-review/SKILL.md index 657a80b71..7b8eeb9bd 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -747,13 +747,15 @@ Comparison boards are local HTML files: open them with `open file://...` on macO If `DESIGN_READY`: the design binary is available for visual mockup generation. Commands: -- `$D generate --brief "..." --output /path.png` — generate a single mockup -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants -- `$D compare --images "a.png,b.png,c.png" --output /path/board.html --serve` — comparison board + HTTP server +- `$D generate --brief "..." --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D compare --images-file /path/board-images.json --output /path/board.html --serve` — comparison board + HTTP server - `$D serve --html /path/board.html` — serve comparison board and collect feedback via HTTP - `$D check --image /path.png --brief "..."` — vision quality gate - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate +Image commands never overwrite (a taken name gets `-2`) and always print JSON (`requested`, `saved`, `failures`); exit 0 ready, 2 nothing saved, 3 stopped after saving some. Capture without `set -e`: `_OUT=$($D ...); _RC=$?`. + **Path rule:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. @@ -1750,9 +1752,12 @@ For each fixable finding, in impact order: If the gstack designer is available and the finding involves visual layout, hierarchy, or spacing (not just a CSS value fix like wrong color or font-size), generate a target mockup showing what the corrected version should look like: ```bash -$D generate --brief "" --output "$REPORT_DIR/screenshots/finding-NNN-target.png" +_OUT=$($D generate --brief "" --output "$REPORT_DIR/screenshots/finding-NNN-target.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` +Record the printed `outputPath` for this finding (images are never overwritten, so it may be bumped); later steps use that path. Exit 2: no mockup was saved; skip it for this finding. + Show the user: "Here's the current state (screenshot) and here's what it should look like (mockup). Now I'll fix the source to match." This step is optional — skip for trivial CSS fixes (wrong hex color, missing padding value). Use it for findings where the intended design isn't obvious from the description alone. @@ -1842,7 +1847,7 @@ DESIGN-FIX RISK: After all fixes are applied: 1. Re-run the design audit on all affected pages -2. If target mockups were generated during the fix loop AND `DESIGN_READY`: run `$D verify --mockup "$REPORT_DIR/screenshots/finding-NNN-target.png" --screenshot "$REPORT_DIR/screenshots/finding-NNN-after.png"` to compare the fix result against the target. Include pass/fail in the report. +2. If target mockups were generated during the fix loop AND `DESIGN_READY`: run `$D verify --mockup "" --screenshot "$REPORT_DIR/screenshots/finding-NNN-after.png"` to compare the fix result against the target. Include pass/fail in the report. 3. Compute final design score and AI slop score 4. Recompute the detector count the same way Phase 0 scanned: DOM mode re-dumps the affected pages after reload into `$REPORT_DIR/dom/$RUN_ID/` and rescans; source mode rescans the files you touched. Then delete `$REPORT_DIR/dom/$RUN_ID/` unless the user passed `--keep-dom`. 5. **If final scores are WORSE than baseline:** WARN prominently — something regressed diff --git a/design-review/SKILL.md.tmpl b/design-review/SKILL.md.tmpl index 6d7d70f0e..5b01fd0f0 100644 --- a/design-review/SKILL.md.tmpl +++ b/design-review/SKILL.md.tmpl @@ -179,9 +179,12 @@ For each fixable finding, in impact order: If the gstack designer is available and the finding involves visual layout, hierarchy, or spacing (not just a CSS value fix like wrong color or font-size), generate a target mockup showing what the corrected version should look like: ```bash -$D generate --brief "" --output "$REPORT_DIR/screenshots/finding-NNN-target.png" +_OUT=$($D generate --brief "" --output "$REPORT_DIR/screenshots/finding-NNN-target.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` +Record the printed `outputPath` for this finding (images are never overwritten, so it may be bumped); later steps use that path. Exit 2: no mockup was saved; skip it for this finding. + Show the user: "Here's the current state (screenshot) and here's what it should look like (mockup). Now I'll fix the source to match." This step is optional — skip for trivial CSS fixes (wrong hex color, missing padding value). Use it for findings where the intended design isn't obvious from the description alone. @@ -271,7 +274,7 @@ DESIGN-FIX RISK: After all fixes are applied: 1. Re-run the design audit on all affected pages -2. If target mockups were generated during the fix loop AND `DESIGN_READY`: run `$D verify --mockup "$REPORT_DIR/screenshots/finding-NNN-target.png" --screenshot "$REPORT_DIR/screenshots/finding-NNN-after.png"` to compare the fix result against the target. Include pass/fail in the report. +2. If target mockups were generated during the fix loop AND `DESIGN_READY`: run `$D verify --mockup "" --screenshot "$REPORT_DIR/screenshots/finding-NNN-after.png"` to compare the fix result against the target. Include pass/fail in the report. 3. Compute final design score and AI slop score 4. Recompute the detector count the same way Phase 0 scanned: DOM mode re-dumps the affected pages after reload into `$REPORT_DIR/dom/$RUN_ID/` and rescans; source mode rescans the files you touched. Then delete `$REPORT_DIR/dom/$RUN_ID/` unless the user passed `--keep-dom`. 5. **If final scores are WORSE than baseline:** WARN prominently — something regressed diff --git a/design-shotgun/SKILL.md b/design-shotgun/SKILL.md index 336cf6ccc..14ce0e5d8 100644 --- a/design-shotgun/SKILL.md +++ b/design-shotgun/SKILL.md @@ -420,13 +420,15 @@ Comparison boards are local HTML files: open them with `open file://...` on macO If `DESIGN_READY`: the design binary is available for visual mockup generation. Commands: -- `$D generate --brief "..." --output /path.png` — generate a single mockup -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants -- `$D compare --images "a.png,b.png,c.png" --output /path/board.html --serve` — comparison board + HTTP server +- `$D generate --brief "..." --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D compare --images-file /path/board-images.json --output /path/board.html --serve` — comparison board + HTTP server - `$D serve --html /path/board.html` — serve comparison board and collect feedback via HTTP - `$D check --image /path.png --brief "..."` — vision quality gate - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate +Image commands never overwrite (a taken name gets `-2`) and always print JSON (`requested`, `saved`, `failures`); exit 0 ready, 2 nothing saved, 3 stopped after saving some. Capture without `set -e`: `_OUT=$($D ...); _RC=$?`. + **Path rule:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. @@ -564,7 +566,7 @@ fonts [top-3], colors [top-3], layouts [top-3], aesthetics [top-3]. Bias generation toward these unless the user explicitly requests a different direction. Also avoid their strong rejections: [top-3 rejected per dimension]." -**Legacy fallback:** Glob `$GSTACK_STATE_ROOT/projects/$SLUG/designs/**/approved.json` (resolve the root with gstack-paths); Read the five newest. Use explicit feedback only, never infer fonts/colors from variant letters. No usable files: continue without a taste profile. +**Legacy fallback:** Glob `$GSTACK_STATE_ROOT/projects/$SLUG/designs/**/approved.json` (resolve the root with gstack-paths); Read the five newest. To view an approved image, resolve it with `~/.claude/skills/gstack/bin/gstack-design-approved `. Use explicit feedback only, never infer fonts/colors from variant letters. No usable files: continue without a taste profile. **Conflict handling:** If the current user request contradicts a strong persistent signal (e.g., "make it playful" when taste profile strongly prefers minimal), flag @@ -588,14 +590,16 @@ _TASTE=$(find "$GSTACK_STATE_ROOT/projects/$SLUG/designs/" -name "approved.json" ``` If prior sessions exist, read each `approved.json` and extract patterns from the -approved variants. Merge these into the taste-profile.json-derived signal — if the +approved variants; resolve each approved image with `~/.claude/skills/gstack/bin/gstack-design-approved ` +(an error means that image is gone: skip it, never substitute another). Merge these into the taste-profile.json-derived signal — if the profile already says "user prefers Geist font" (from aggregated history), the approved.json files add the specific recent approval context. Limit to last 10 sessions. Try/catch JSON parse on each (skip corrupted files). **Updating taste profile after a design-shotgun session:** When the user picks a -variant, call `~/.claude/skills/gstack/bin/gstack-taste-update approved `. When they +variant, call `~/.claude/skills/gstack/bin/gstack-taste-update approved ` (the +`APPROVED_IMAGE` printed when approved.json is saved). When they explicitly reject a variant, call `~/.claude/skills/gstack/bin/gstack-taste-update rejected `. The CLI handles schema migration from approved.json, decay, and conflict flagging. @@ -679,24 +683,6 @@ variant (parallel foreground calls in one message still run concurrently, and th comparison board needs every variant's result). Each agent is independent and handles its own generation, quality check, verification, and retry. -**Reserve this round's directory first.** Every generation round, including each -regenerate round, writes to its own `round-/` so earlier paid variants are never -overwritten. Substitute Step 3's `DESIGN_DIR`: - -```bash -_DESIGN_DIR="" -mkdir -p "$_DESIGN_DIR" || exit 1 -_N=1 -until mkdir "$_DESIGN_DIR/round-$_N" 2>/dev/null; do - _N=$((_N + 1)); [ "$_N" -le 999 ] || { echo "ROUND_DIR_FAILED: $_DESIGN_DIR"; exit 1; } -done -_ROUND_TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-variants.XXXXXX") || exit 1 -echo "ROUND_DIR: $_DESIGN_DIR/round-$_N" -echo "ROUND_TMP: $_ROUND_TMP" -``` - -Substitute the printed `ROUND_DIR` and `ROUND_TMP` into every agent prompt below. - **Important: $D path propagation.** The `$D` variable from DESIGN SETUP is a shell variable that agents do NOT inherit. Substitute the resolved absolute path (from the `DESIGN_READY: /path/to/design` output in Step 0) into each agent prompt. @@ -704,61 +690,55 @@ variable that agents do NOT inherit. Substitute the resolved absolute path (from **Agent prompt template** (one per variant, substitute all `{...}` values): ``` -Generate a design variant and save it. +Generate a design variant and publish it without overwriting any image. Design binary: {absolute path to $D binary} +Claim helper: ~/.claude/skills/gstack/bin/gstack-design-claim Brief: {the full variant-specific brief for this direction} -Output: {ROUND_TMP}/variant-{letter}.png -Final location: {ROUND_DIR}/variant-{letter}.png +Design dir: {_DESIGN_DIR absolute path} -Steps: -1. Run: {$D path} generate --brief "{brief}" --output {ROUND_TMP}/variant-{letter}.png -2. If the command fails with a rate limit error (429 or "rate limit"), wait 5 seconds - and retry. Up to 3 retries. -3. If the output file is missing or empty after the command succeeds, retry once. -4. Copy: cp {ROUND_TMP}/variant-{letter}.png {ROUND_DIR}/variant-{letter}.png -5. Quality check: {$D path} check --image {ROUND_DIR}/variant-{letter}.png --brief "{brief}" - If quality check fails, retry generation once. -6. Verify: ls -lh {ROUND_DIR}/variant-{letter}.png -7. Report exactly one of: - VARIANT_{letter}_DONE: {file size} - VARIANT_{letter}_FAILED: {error description} +Steps (no set -e; never cp or mv an image): +1. Stage in a fresh per-run directory: STAGE=$(mktemp -d "${TMPDIR:-/tmp}/gstack-variant-XXXXXXXX") +2. Generate: OUT=$({$D path} generate --brief "{brief}" --output "$STAGE/variant.png"); RC=$?; echo "$OUT"; echo "EXIT: $RC" + Read outputPath, saved and failures from the JSON. Exit 2 = nothing saved: on a rate + limit (429 or "rate limit") wait 5 seconds and retry, up to 3 retries. +3. Publish: FINAL=$(~/.claude/skills/gstack/bin/gstack-design-claim "" "{Design dir}/variant-{letter}.png") + It never overwrites and prints the final (possibly bumped) path. Use FINAL from here on. + If it fails, report the error; the staged image stays at . +4. Quality check: {$D path} check --image "" --brief "{brief}" + If it fails, regenerate once (steps 2-3; the first image is kept). +5. Verify: ls -lh "" +6. Report exactly one of: + VARIANT_{letter}_DONE: {every published path, comma-separated, last one is the pick} {file size} + VARIANT_{letter}_FAILED: {error description, plus any published paths} VARIANT_{letter}_RATE_LIMITED: exhausted retries ``` -For the evolve path, replace step 1 with: +For the evolve path, replace the step 2 command with: ``` -{$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output {ROUND_TMP}/variant-{letter}.png +OUT=$({$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output "$STAGE/variant.png"); RC=$? ``` -**Generate to `ROUND_TMP`, then `cp`:** in sandboxed sessions `$D generate --output` under -`~/.gstack/` can abort ("The operation was aborted"), while the temp dir works. `ROUND_TMP` -is private to this round, so concurrent sessions never share a file. +**Why staging:** in sandboxed sessions `$D generate --output` under `~/.gstack/` can +abort ("The operation was aborted"), while the temp dir works. A fresh `mktemp -d` per run +means no other session's file can be published, and the claim helper never clobbers. ### Step 3d: Results After all agents complete: -1. Read each generated PNG inline (Read tool) so the user sees all variants at once. -2. Report status: "All {N} variants generated in ~{actual time}. {successes} succeeded, - {failures} failed." -3. For any failures: report explicitly with the error. Do NOT silently skip. -4. If zero variants succeeded: fall back to sequential generation (one at a time with - `$D generate`, showing each as it lands). Tell the user: "Parallel generation failed - (likely rate limiting). Falling back to sequential..." -5. Proceed to Step 4 (comparison board). - -**Dynamic image list for comparison board:** When proceeding to Step 4, construct the -image list from whatever variant files actually exist, not a hardcoded A/B/C list: - -```bash -setopt +o nomatch 2>/dev/null || true # zsh compat -_IMAGES=$(ls ""/variant-*.png 2>/dev/null | tr '\n' ',' | sed 's/,$//') -``` - -Use `$_IMAGES` in the `$D compare --images` command. A regenerate round run through -`$D variants --output-dir "$_DESIGN_DIR/"` reserves the next `round-/` itself and -prints `roundDir`, `manifest` and the saved `paths`; build that round's board from `paths`. + +1. Round accounting first: this round's images are exactly the paths the DONE and FAILED + lines report (never a directory listing; older rounds stay on disk). Tell the user: + "{saved} of {N} paid images saved in ~{actual time}", listing every published path. +2. For any failures: report explicitly with the error. Do NOT silently skip. +3. If zero variants succeeded: fall back to sequential generation, running the agent + steps yourself one variant at a time and showing each as it lands. Tell the user: + "Parallel generation failed (likely rate limiting). Falling back to sequential..." + If that also saves nothing, report the failures and stop: no board. +4. Read each saved image inline (Read tool, the reported paths) so the user sees all variants at once. +5. Proceed to Step 4 with each variant's pick (the last reported path), in letter order, + as this round's board images. ## Step 4: Comparison Board + Feedback Loop @@ -766,8 +746,12 @@ prints `roundDir`, `manifest` and the saved `paths`; build that round's board fr Create the comparison board and serve it over HTTP: + +Write this round's board images (printed paths that passed checks, in order) as a JSON array to `$_DESIGN_DIR/board-images.json` with the Write tool; board letters A, B, C follow that order. Then archive any earlier Submit so it cannot approve these images, and build the board: + ```bash -$D compare --images "" --output "$_DESIGN_DIR/design-board.html" --serve +[ -f "${_DESIGN_DIR:?set _DESIGN_DIR to the design dir printed above}/feedback.json" ] && mv "${_DESIGN_DIR:?}/feedback.json" "${_DESIGN_DIR:?}/feedback-$(date -u +%Y%m%dT%H%M%SZ).json" +$D compare --images-file "$_DESIGN_DIR/board-images.json" --output "$_DESIGN_DIR/design-board.html" --serve ``` Creates HTML and opens the board. **Run it in the background** (host task, or `&` redirecting stdout/stderr to private files in `$_DESIGN_DIR`). Read captured stderr for the startup marker; a PID is not readiness. Missing marker: use the failure fallback below. @@ -826,8 +810,8 @@ the approved variant. 1. Read `regenerateAction` from the JSON (`"different"`, `"match"`, `"more_like_B"`, `"remix"`, or custom text) 2. If `regenerateAction` is `"remix"`, read `remixSpec` (e.g. `{"layout":"A","colors":"B"}`) -3. Generate new variants with `$D iterate` or `$D variants` using updated brief -4. Create new board: `$D compare --images "..." --output "$_DESIGN_DIR/design-board.html"` +3. Generate new variants with `$D iterate` or `$D variants` using updated brief (capture the JSON and do round accounting as for the first round) +4. Rebuild with the board block above (it archives feedback.json and rewrites board-images.json), without `--serve` 5. Reload the board in the user's browser (same tab) — the URL is per-board under daemon mode, so use `` (from the `BOARD_URL:` stderr line) as the base: @@ -861,9 +845,16 @@ Is this right?" Use AskUserQuestion to verify before proceeding. -**Save the approved choice:** +**Save the approved choice.** Map the confirmed letter through this board's `board-images.json` (never the directory listing) and save it: + ```bash -echo '{"approved_variant":"","approved_path":"","round":,"feedback":"","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" +_IMG=$(jq -r --arg v "" '.[($v | explode[0]) - 65] // empty' "$_DESIGN_DIR/board-images.json") +if [ -n "$_IMG" ]; then + echo '{"approved_variant":"","approved_path":"'"$(basename "$_IMG")"'","feedback":"","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" + echo "APPROVED_IMAGE: $_IMG" +else + echo "NO_BOARD_IMAGE: is not on this board; reselect from the board" +fi ``` ## Step 5: Feedback Confirmation @@ -884,13 +875,11 @@ Use AskUserQuestion to confirm before saving. ## Step 6: Save & Next Steps -Write `approved.json` to `$_DESIGN_DIR/` (handled by the loop above). Also record -`approved_path`, the absolute path of the chosen PNG, and its `round`. The choice may -come from an earlier round (for example round 1's B after round 2); earlier rounds stay -on disk, and `round-/manifest.json` (from `$D variants`) lists what each round saved. +Write `approved.json` to `$_DESIGN_DIR/` (handled by the loop above). If invoked from another skill: return the structured feedback for that skill to consume. -The calling skill reads `approved.json` and the approved variant PNG. +The calling skill reads `approved.json` and resolves the approved image with +`~/.claude/skills/gstack/bin/gstack-design-approved "$_DESIGN_DIR/approved.json"`. If standalone, offer next steps via AskUserQuestion: diff --git a/design-shotgun/SKILL.md.tmpl b/design-shotgun/SKILL.md.tmpl index 70c26e583..fe4e0d309 100644 --- a/design-shotgun/SKILL.md.tmpl +++ b/design-shotgun/SKILL.md.tmpl @@ -173,14 +173,16 @@ _TASTE=$(find "$GSTACK_STATE_ROOT/projects/$SLUG/designs/" -name "approved.json" ``` If prior sessions exist, read each `approved.json` and extract patterns from the -approved variants. Merge these into the taste-profile.json-derived signal — if the +approved variants; resolve each approved image with `{{BIN_DIR}}/gstack-design-approved ` +(an error means that image is gone: skip it, never substitute another). Merge these into the taste-profile.json-derived signal — if the profile already says "user prefers Geist font" (from aggregated history), the approved.json files add the specific recent approval context. Limit to last 10 sessions. Try/catch JSON parse on each (skip corrupted files). **Updating taste profile after a design-shotgun session:** When the user picks a -variant, call `{{BIN_DIR}}/gstack-taste-update approved `. When they +variant, call `{{BIN_DIR}}/gstack-taste-update approved ` (the +`APPROVED_IMAGE` printed when approved.json is saved). When they explicitly reject a variant, call `{{BIN_DIR}}/gstack-taste-update rejected `. The CLI handles schema migration from approved.json, decay, and conflict flagging. @@ -264,24 +266,6 @@ variant (parallel foreground calls in one message still run concurrently, and th comparison board needs every variant's result). Each agent is independent and handles its own generation, quality check, verification, and retry. -**Reserve this round's directory first.** Every generation round, including each -regenerate round, writes to its own `round-/` so earlier paid variants are never -overwritten. Substitute Step 3's `DESIGN_DIR`: - -```bash -_DESIGN_DIR="" -mkdir -p "$_DESIGN_DIR" || exit 1 -_N=1 -until mkdir "$_DESIGN_DIR/round-$_N" 2>/dev/null; do - _N=$((_N + 1)); [ "$_N" -le 999 ] || { echo "ROUND_DIR_FAILED: $_DESIGN_DIR"; exit 1; } -done -_ROUND_TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-variants.XXXXXX") || exit 1 -echo "ROUND_DIR: $_DESIGN_DIR/round-$_N" -echo "ROUND_TMP: $_ROUND_TMP" -``` - -Substitute the printed `ROUND_DIR` and `ROUND_TMP` into every agent prompt below. - **Important: $D path propagation.** The `$D` variable from DESIGN SETUP is a shell variable that agents do NOT inherit. Substitute the resolved absolute path (from the `DESIGN_READY: /path/to/design` output in Step 0) into each agent prompt. @@ -289,61 +273,55 @@ variable that agents do NOT inherit. Substitute the resolved absolute path (from **Agent prompt template** (one per variant, substitute all `{...}` values): ``` -Generate a design variant and save it. +Generate a design variant and publish it without overwriting any image. Design binary: {absolute path to $D binary} +Claim helper: {{BIN_DIR}}/gstack-design-claim Brief: {the full variant-specific brief for this direction} -Output: {ROUND_TMP}/variant-{letter}.png -Final location: {ROUND_DIR}/variant-{letter}.png +Design dir: {_DESIGN_DIR absolute path} -Steps: -1. Run: {$D path} generate --brief "{brief}" --output {ROUND_TMP}/variant-{letter}.png -2. If the command fails with a rate limit error (429 or "rate limit"), wait 5 seconds - and retry. Up to 3 retries. -3. If the output file is missing or empty after the command succeeds, retry once. -4. Copy: cp {ROUND_TMP}/variant-{letter}.png {ROUND_DIR}/variant-{letter}.png -5. Quality check: {$D path} check --image {ROUND_DIR}/variant-{letter}.png --brief "{brief}" - If quality check fails, retry generation once. -6. Verify: ls -lh {ROUND_DIR}/variant-{letter}.png -7. Report exactly one of: - VARIANT_{letter}_DONE: {file size} - VARIANT_{letter}_FAILED: {error description} +Steps (no set -e; never cp or mv an image): +1. Stage in a fresh per-run directory: STAGE=$(mktemp -d "${TMPDIR:-/tmp}/gstack-variant-XXXXXXXX") +2. Generate: OUT=$({$D path} generate --brief "{brief}" --output "$STAGE/variant.png"); RC=$?; echo "$OUT"; echo "EXIT: $RC" + Read outputPath, saved and failures from the JSON. Exit 2 = nothing saved: on a rate + limit (429 or "rate limit") wait 5 seconds and retry, up to 3 retries. +3. Publish: FINAL=$({{BIN_DIR}}/gstack-design-claim "" "{Design dir}/variant-{letter}.png") + It never overwrites and prints the final (possibly bumped) path. Use FINAL from here on. + If it fails, report the error; the staged image stays at . +4. Quality check: {$D path} check --image "" --brief "{brief}" + If it fails, regenerate once (steps 2-3; the first image is kept). +5. Verify: ls -lh "" +6. Report exactly one of: + VARIANT_{letter}_DONE: {every published path, comma-separated, last one is the pick} {file size} + VARIANT_{letter}_FAILED: {error description, plus any published paths} VARIANT_{letter}_RATE_LIMITED: exhausted retries ``` -For the evolve path, replace step 1 with: +For the evolve path, replace the step 2 command with: ``` -{$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output {ROUND_TMP}/variant-{letter}.png +OUT=$({$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output "$STAGE/variant.png"); RC=$? ``` -**Generate to `ROUND_TMP`, then `cp`:** in sandboxed sessions `$D generate --output` under -`~/.gstack/` can abort ("The operation was aborted"), while the temp dir works. `ROUND_TMP` -is private to this round, so concurrent sessions never share a file. +**Why staging:** in sandboxed sessions `$D generate --output` under `~/.gstack/` can +abort ("The operation was aborted"), while the temp dir works. A fresh `mktemp -d` per run +means no other session's file can be published, and the claim helper never clobbers. ### Step 3d: Results After all agents complete: -1. Read each generated PNG inline (Read tool) so the user sees all variants at once. -2. Report status: "All {N} variants generated in ~{actual time}. {successes} succeeded, - {failures} failed." -3. For any failures: report explicitly with the error. Do NOT silently skip. -4. If zero variants succeeded: fall back to sequential generation (one at a time with - `$D generate`, showing each as it lands). Tell the user: "Parallel generation failed - (likely rate limiting). Falling back to sequential..." -5. Proceed to Step 4 (comparison board). - -**Dynamic image list for comparison board:** When proceeding to Step 4, construct the -image list from whatever variant files actually exist, not a hardcoded A/B/C list: - -```bash -setopt +o nomatch 2>/dev/null || true # zsh compat -_IMAGES=$(ls ""/variant-*.png 2>/dev/null | tr '\n' ',' | sed 's/,$//') -``` - -Use `$_IMAGES` in the `$D compare --images` command. A regenerate round run through -`$D variants --output-dir "$_DESIGN_DIR/"` reserves the next `round-/` itself and -prints `roundDir`, `manifest` and the saved `paths`; build that round's board from `paths`. + +1. Round accounting first: this round's images are exactly the paths the DONE and FAILED + lines report (never a directory listing; older rounds stay on disk). Tell the user: + "{saved} of {N} paid images saved in ~{actual time}", listing every published path. +2. For any failures: report explicitly with the error. Do NOT silently skip. +3. If zero variants succeeded: fall back to sequential generation, running the agent + steps yourself one variant at a time and showing each as it lands. Tell the user: + "Parallel generation failed (likely rate limiting). Falling back to sequential..." + If that also saves nothing, report the failures and stop: no board. +4. Read each saved image inline (Read tool, the reported paths) so the user sees all variants at once. +5. Proceed to Step 4 with each variant's pick (the last reported path), in letter order, + as this round's board images. ## Step 4: Comparison Board + Feedback Loop @@ -367,13 +345,11 @@ Use AskUserQuestion to confirm before saving. ## Step 6: Save & Next Steps -Write `approved.json` to `$_DESIGN_DIR/` (handled by the loop above). Also record -`approved_path`, the absolute path of the chosen PNG, and its `round`. The choice may -come from an earlier round (for example round 1's B after round 2); earlier rounds stay -on disk, and `round-/manifest.json` (from `$D variants`) lists what each round saved. +Write `approved.json` to `$_DESIGN_DIR/` (handled by the loop above). If invoked from another skill: return the structured feedback for that skill to consume. -The calling skill reads `approved.json` and the approved variant PNG. +The calling skill reads `approved.json` and resolves the approved image with +`{{BIN_DIR}}/gstack-design-approved "$_DESIGN_DIR/approved.json"`. If standalone, offer next steps via AskUserQuestion: diff --git a/design/src/approval.ts b/design/src/approval.ts new file mode 100644 index 000000000..400015439 --- /dev/null +++ b/design/src/approval.ts @@ -0,0 +1,56 @@ +/** + * Resolve the image an approval record names. `approved_path` is stored + * relative to the directory holding approved.json, so a copied or relocated + * designs directory still resolves. Records without `approved_path` (written + * before never-overwrite) fall back to `variant-.png`. A + * present `approved_path` whose file is missing is an error: another image is + * never substituted. + */ + +import fs from "fs"; +import path from "path"; + +export type ApprovedImage = { ok: true; path: string; legacy: boolean } | { ok: false; error: string }; + +export interface ApprovalRecord { + approved_variant?: unknown; + approved_path?: unknown; +} + +function missing(imagePath: string): ApprovedImage { + return { ok: false, error: `approved image ${imagePath} is missing; reselect from the board` }; +} + +function readable(imagePath: string): boolean { + try { + fs.accessSync(imagePath, fs.constants.R_OK); + return fs.statSync(imagePath).isFile(); + } catch { + return false; + } +} + +/** The image path a record names, without checking that it exists. */ +export function approvedImagePath(record: ApprovalRecord, recordDir: string): { path: string; legacy: boolean } | null { + if (typeof record.approved_path === "string" && record.approved_path !== "") { + return { path: path.resolve(recordDir, record.approved_path), legacy: false }; + } + if (typeof record.approved_variant === "string" && record.approved_variant !== "") { + return { path: path.join(recordDir, `variant-${record.approved_variant}.png`), legacy: true }; + } + return null; +} + +export function resolveApprovedImage(approvedJsonPath: string): ApprovedImage { + let record: ApprovalRecord; + try { + record = JSON.parse(fs.readFileSync(approvedJsonPath, "utf-8")); + } catch (err: any) { + return { ok: false, error: `cannot read ${approvedJsonPath}: ${err.code || err.message}` }; + } + const named = approvedImagePath(record ?? {}, path.dirname(path.resolve(approvedJsonPath))); + if (!named) { + return { ok: false, error: `${approvedJsonPath} names no approved image; reselect from the board` }; + } + return readable(named.path) ? { ok: true, ...named } : missing(named.path); +} diff --git a/design/src/auth.ts b/design/src/auth.ts index 1202500a9..fc25cbcf4 100644 --- a/design/src/auth.ts +++ b/design/src/auth.ts @@ -119,19 +119,20 @@ export function saveApiKey(key: string): void { } /** - * Get API key or exit with setup instructions. + * Get API key or throw with setup instructions. */ export function requireApiKey(): string { const resolution = resolveApiKeyInfo(); if (!resolution) { - console.error("No OpenAI API key found."); - console.error(""); - console.error("Run: $D setup"); - console.error(" or save to ~/.gstack/openai.json: { \"api_key\": \"sk-...\" }"); - console.error(" or set OPENAI_API_KEY environment variable"); - console.error(""); - console.error("Get a key at: https://platform.openai.com/api-keys"); - process.exit(1); + throw new Error([ + "No OpenAI API key found.", + "", + "Run: $D setup", + " or save to ~/.gstack/openai.json: { \"api_key\": \"sk-...\" }", + " or set OPENAI_API_KEY environment variable", + "", + "Get a key at: https://platform.openai.com/api-keys", + ].join("\n")); } console.error(`Using OpenAI key from ${describeApiKeySource(resolution)}.`); if (resolution.warning) console.error(resolution.warning); diff --git a/design/src/cli.ts b/design/src/cli.ts index d6d8bcc47..02ac04527 100644 --- a/design/src/cli.ts +++ b/design/src/cli.ts @@ -12,7 +12,7 @@ * 4. Print result JSON to stdout */ -import { COMMANDS } from "./commands"; +import { COMMANDS, OUTPUT_CONTRACT } from "./commands"; import { generate } from "./generate"; import { checkCommand } from "./check"; import { compare } from "./compare"; @@ -27,6 +27,7 @@ import { serve } from "./serve"; import { gallery } from "./gallery"; import { normalizeIntFlag } from "./flag-utils"; import { readVersionString } from "./daemon-state"; +import { resolveImagePaths } from "./image-args"; import { daemonStatus as daemonStatusClient, ensureDaemon, @@ -34,6 +35,9 @@ import { shutdownDaemon, } from "./daemon-client"; import { spawn as nodeSpawn } from "child_process"; +import fs from "fs"; +import os from "os"; +import path from "path"; function parseArgs(argv: string[]): { command: string; @@ -81,6 +85,7 @@ function printUsage(): void { console.log(` ${name.padEnd(12)} ${info.description}`); console.log(` ${"".padEnd(12)} ${info.usage}`); } + console.log(`\n${OUTPUT_CONTRACT}`); console.log("\nAuth: ~/.gstack/openai.json, then OPENAI_API_KEY env var"); console.log("If OPENAI_API_KEY matches a current-directory .env file, the source is reported before billing."); console.log("Setup: $D setup"); @@ -113,19 +118,18 @@ async function runSetup(): Promise { // Smoke test console.log("\nRunning smoke test (generating a simple image)..."); - try { - await generate({ - brief: "A simple blue square centered on a white background. Minimal, geometric, clean.", - output: "/tmp/gstack-design-smoke-test.png", - size: "1024x1024", - quality: "low", - }); - console.log("\nSmoke test PASSED. Design generation is working."); - } catch (err: any) { - console.error(`\nSmoke test FAILED: ${err.message}`); + const smoke = await generate({ + brief: "A simple blue square centered on a white background. Minimal, geometric, clean.", + output: path.join(fs.mkdtempSync(path.join(os.tmpdir(), "gstack-design-smoke-")), "smoke-test.png"), + size: "1024x1024", + quality: "low", + }); + if (smoke.exitCode !== 0) { + console.error(`\nSmoke test FAILED: ${smoke.failures.map(f => f.reason).join("; ")}`); console.error("Check your API key and organization verification status."); process.exit(1); } + console.log("\nSmoke test PASSED. Design generation is working."); } async function main(): Promise { @@ -139,7 +143,7 @@ async function main(): Promise { switch (command) { case "generate": - await generate({ + process.exitCode = (await generate({ brief: flags.brief as string, briefFile: flags["brief-file"] as string, output: (flags.output as string) || "/tmp/gstack-mockup.png", @@ -147,7 +151,7 @@ async function main(): Promise { retry: normalizeIntFlag(flags.retry, { name: "retry", def: 0, min: 0 }), size: flags.size as string, quality: flags.quality as string, - }); + })).exitCode; break; case "check": @@ -155,9 +159,7 @@ async function main(): Promise { break; case "compare": { - // Parse --images as glob or multiple files - const imagesArg = flags.images as string; - const images = await resolveImagePaths(imagesArg); + const images = await resolveImagePaths(flags.images as string, flags["images-file"] as string | undefined); const outputPath = (flags.output as string) || "/tmp/gstack-design-board.html"; compare({ images, output: outputPath }); // If --serve flag is set, publish the board. @@ -201,7 +203,7 @@ async function main(): Promise { break; case "variants": - await variants({ + process.exitCode = await variants({ brief: flags.brief as string, briefFile: flags["brief-file"] as string, // #2032: pass the RAW flag through — variants() normalizes at its @@ -215,7 +217,7 @@ async function main(): Promise { break; case "iterate": - await iterate({ + process.exitCode = await iterate({ session: flags.session as string, feedback: flags.feedback as string, output: (flags.output as string) || "/tmp/gstack-iterate.png", @@ -267,7 +269,7 @@ async function main(): Promise { } case "evolve": - await evolve({ + process.exitCode = await evolve({ screenshot: flags.screenshot as string, brief: flags.brief as string, output: (flags.output as string) || "/tmp/gstack-evolved.png", @@ -387,31 +389,6 @@ function openBrowser(url: string): void { } } -/** - * Resolve image paths from a glob pattern or comma-separated list. - */ -async function resolveImagePaths(input: string): Promise { - if (!input) { - console.error("--images is required. Provide glob pattern or comma-separated paths."); - process.exit(1); - } - - // Check if it's a glob pattern - if (input.includes("*")) { - const glob = new Bun.Glob(input); - const paths: string[] = []; - for await (const match of glob.scan({ absolute: true })) { - if (match.endsWith(".png") || match.endsWith(".jpg") || match.endsWith(".jpeg")) { - paths.push(match); - } - } - return paths.sort(); - } - - // Comma-separated or single path - return input.split(",").map(p => p.trim()); -} - // Self-execution shortcut: when invoked with --daemon-mode, this same // binary runs as the persistent design daemon instead of the CLI. Keeps // the production install to a single executable; daemon-client.ts spawns diff --git a/design/src/commands.ts b/design/src/commands.ts index 17d5811a7..76ff9ded7 100644 --- a/design/src/commands.ts +++ b/design/src/commands.ts @@ -16,17 +16,17 @@ export const COMMANDS = new Map([ ["generate", { description: "Generate a UI mockup from a design brief", - usage: "generate --brief \"...\" --output /path.png", + usage: "generate --brief \"...\" --output /path.png [--check --retry N] (never overwrites: bumps to /path-2.png; each --retry attempt is its own saved image; prints outputPath, attempts + run fields)", flags: ["--brief", "--brief-file", "--output", "--check", "--retry", "--size", "--quality"], }], ["variants", { description: "Generate N design variants from a brief", - usage: "variants --brief \"...\" --count 3 --output-dir /path/", + usage: "variants --brief \"...\" --count 3 --output-dir /path/ [--viewports desktop,tablet,mobile] (never overwrites: variant-A.png bumps to variant-A-2.png; prints paths, errors + run fields)", flags: ["--brief", "--brief-file", "--count", "--output-dir", "--size", "--quality", "--viewports"], }], ["iterate", { description: "Iterate on an existing mockup with feedback", - usage: "iterate --session /path/session.json --feedback \"...\" --output /path.png", + usage: "iterate --session /path/session.json --feedback \"...\" --output /path.png (never overwrites; prints outputPath + run fields)", flags: ["--session", "--feedback", "--output"], }], ["check", { @@ -36,8 +36,8 @@ export const COMMANDS = new Map x-2.png, x-2.png -> x-2-2.png), stderr prints +"note: exists; saved to (existing file kept)", and the JSON +reports the actual path. Read paths from the JSON, never from a fixed file name. + +Run fields printed by generate, iterate, evolve and variants (on every exit): + requested images requested saved every saved path, in order + selected outputPath or paths failures [{file, reason}] not saved + recovered [{path, reason}] private recovery copies in the temp dir + + command extra fields readers + generate outputPath sessionFile responseId checkResult design-review, design-shotgun, + attempts[{path, check}] (one per --retry try) plan-design-review + iterate outputPath sessionFile responseId iteration plan-design-review, design-consultation + evolve outputPath sourceScreenshot brief attempts design-shotgun + variants outputDir count succeeded failed paths errors office-hours, plan-design-review, + viewports (--viewports only) design-consultation + compare board HTML; --serve prints {id, url, sourceDir} all board flows (--images-file) + +Exit codes: 0 a selected result exists; 2 nothing was saved; 3 stopped after +saving at least one image but before its selected result (interrupted retry); +1 invalid flags rejected before any API call (no JSON).`; diff --git a/design/src/evolve.ts b/design/src/evolve.ts index a656054b0..69b486849 100644 --- a/design/src/evolve.ts +++ b/design/src/evolve.ts @@ -6,10 +6,10 @@ */ import fs from "fs"; -import path from "path"; import { requireApiKey } from "./auth"; import { receiptedFetch } from "./receipted-fetch"; import { imageRequestBody, modelRejectionHint, visionRequestBody } from "./models"; +import { emitResult, exitCodeFor, newAccounting, persistImage, recordOutcome, type ExitCode } from "./persist"; export interface EvolveOptions { screenshot: string; // Path to current site screenshot @@ -22,37 +22,68 @@ export interface EvolveOptions { * Sends the screenshot as context to the image model with image generation, * asking it to produce a new version incorporating the brief's changes. */ -export async function evolve(options: EvolveOptions): Promise { - const apiKey = requireApiKey(); - const screenshotData = fs.readFileSync(options.screenshot).toString("base64"); +export async function evolve(options: EvolveOptions): Promise { + const acct = newAccounting(1); + let outputPath: string | null = null; - console.error(`Evolving ${options.screenshot} with: "${options.brief}"`); - const startTime = Date.now(); + try { + const apiKey = requireApiKey(); + const screenshotData = fs.readFileSync(options.screenshot).toString("base64"); - // Use the Responses API with both a text prompt referencing the screenshot - // and the image_generation tool to produce the evolved version. - // Since we can't send reference images directly to image_generation, - // we describe the current state in detail first via vision, then generate. + console.error(`Evolving ${options.screenshot} with: "${options.brief}"`); + const startTime = Date.now(); - // Step 1: Analyze current screenshot - const analysis = await analyzeScreenshot(apiKey, screenshotData); - console.error(` Analyzed current design: ${analysis.slice(0, 100)}...`); + // Use the Responses API with both a text prompt referencing the screenshot + // and the image_generation tool to produce the evolved version. + // Since we can't send reference images directly to image_generation, + // we describe the current state in detail first via vision, then generate. - // Step 2: Generate evolved version using analysis + brief - const evolvedPrompt = [ - "Generate a pixel-perfect UI mockup that is an improved version of an existing design.", - "", - "CURRENT DESIGN (what exists now):", - analysis, - "", - "REQUESTED CHANGES:", - options.brief, - "", - "Generate a new mockup that keeps the existing layout structure but applies the requested changes.", - "The result should look like a real production UI. All text must be readable.", - "1536x1024 pixels.", - ].join("\n"); + // Step 1: Analyze current screenshot + const analysis = await analyzeScreenshot(apiKey, screenshotData); + console.error(` Analyzed current design: ${analysis.slice(0, 100)}...`); + // Step 2: Generate evolved version using analysis + brief + const evolvedPrompt = [ + "Generate a pixel-perfect UI mockup that is an improved version of an existing design.", + "", + "CURRENT DESIGN (what exists now):", + analysis, + "", + "REQUESTED CHANGES:", + options.brief, + "", + "Generate a new mockup that keeps the existing layout structure but applies the requested changes.", + "The result should look like a real production UI. All text must be readable.", + "1536x1024 pixels.", + ].join("\n"); + + const imageData = await requestEvolvedImage(apiKey, evolvedPrompt); + const outcome = persistImage(imageData, options.output); + outputPath = recordOutcome(acct, outcome); + if (outcome.ok) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + console.error(`Generated (${elapsed}s, ${(outcome.bytes / 1024).toFixed(0)}KB) → ${outcome.path}`); + } + } catch (err: any) { + const reason = err?.message || String(err); + console.error(reason); + acct.failures.push({ file: options.output, reason }); + } + + return emitResult({ + outputPath, + sourceScreenshot: options.screenshot, + brief: options.brief, + attempts: acct.saved.map(p => ({ path: p })), + requested: acct.requested, + saved: acct.saved, + selected: outputPath, + failures: acct.failures, + recovered: acct.recovered, + }, exitCodeFor(outputPath !== null, acct.saved.length)); +} + +async function requestEvolvedImage(apiKey: string, evolvedPrompt: string): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 240_000); @@ -85,19 +116,7 @@ export async function evolve(options: EvolveOptions): Promise { if (!imageItem?.result) { throw new Error("No image data in response"); } - - fs.mkdirSync(path.dirname(options.output), { recursive: true }); - const imageBuffer = Buffer.from(imageItem.result, "base64"); - fs.writeFileSync(options.output, imageBuffer); - - const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); - console.error(`Generated (${elapsed}s, ${(imageBuffer.length / 1024).toFixed(0)}KB) → ${options.output}`); - - console.log(JSON.stringify({ - outputPath: options.output, - sourceScreenshot: options.screenshot, - brief: options.brief, - }, null, 2)); + return imageItem.result; } finally { clearTimeout(timeout); } diff --git a/design/src/gallery.ts b/design/src/gallery.ts index e6defaf96..5ab08a486 100644 --- a/design/src/gallery.ts +++ b/design/src/gallery.ts @@ -6,6 +6,7 @@ import fs from "fs"; import path from "path"; +import { approvedImagePath } from "./approval"; export interface GalleryOptions { designsDir: string; // ~/.gstack/projects/$SLUG/designs/ @@ -20,6 +21,32 @@ interface SessionData { variants: string[]; // paths to variant PNGs } +const VARIANT_IMAGE = [/^variant-[A-Z](-\d+)?\.png$/i, /^variant-\d+(-\d+)?\.png$/i]; + +/** + * Variant images in a session directory as chronological history, not rounds: + * names not on the current board sort by mtime then name, followed by the + * board's images in board-images.json order. Never-overwrite means one + * letter can appear in several rounds (variant-A.png, variant-A-2.png). + */ +export function listVariantImages(sessionDir: string): string[] { + const found = fs.readdirSync(sessionDir) + .filter(f => VARIANT_IMAGE.some(re => re.test(f))) + .map(f => path.join(sessionDir, f)); + + let board: string[] = []; + try { + const parsed = JSON.parse(fs.readFileSync(path.join(sessionDir, "board-images.json"), "utf-8")); + if (Array.isArray(parsed)) board = parsed.filter((p): p is string => typeof p === "string").map(p => path.resolve(sessionDir, p)); + } catch {} + + const mtime = new Map(found.map(f => [f, fs.statSync(f).mtimeMs])); + const history = found + .filter(f => !board.includes(f)) + .sort((a, b) => (mtime.get(a)! - mtime.get(b)!) || a.localeCompare(b)); + return [...history, ...board.filter(f => found.includes(f))]; +} + export function generateGalleryHtml(designsDir: string): string { const sessions: SessionData[] = []; @@ -44,21 +71,9 @@ export function generateGalleryHtml(designsDir: string): string { } } - // Find variant PNGs: legacy flat files plus every round-/ directory - // (`$D variants` writes each regenerate round to its own directory, #1529). - const variants: string[] = []; + let variants: string[] = []; try { - const dirs = [sessionDir, ...fs.readdirSync(sessionDir) - .filter(f => /^round-\d+$/.test(f)) - .map(f => path.join(sessionDir, f))]; - for (const dir of dirs) { - for (const f of fs.readdirSync(dir)) { - if (f.match(/variant-[A-Z]\.png$/i) || f.match(/variant-\d+\.png$/i)) { - variants.push(path.join(dir, f)); - } - } - } - variants.sort(); + variants = listVariantImages(sessionDir); } catch { // Can't read directory, skip } @@ -86,12 +101,13 @@ export function generateGalleryHtml(designsDir: string): string { sessions.sort((a, b) => b.date.localeCompare(a.date)); const sessionCards = sessions.map(session => { + const approvedTarget = session.approved ? approvedImagePath(session.approved, session.dir) : null; const variantImgs = session.variants.map((vPath, i) => { try { const imgData = fs.readFileSync(vPath).toString("base64"); const ext = path.extname(vPath).slice(1) || "png"; const label = path.basename(vPath, `.${ext}`).replace("variant-", ""); - const isApproved = session.approved?.approved_variant === label; + const isApproved = approvedTarget?.path === vPath; return `