diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index 6922787e6..ea3d6829f 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -550,12 +550,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. @@ -662,7 +664,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 090737c77..b4cb37eaa 100644 --- a/design-consultation/sections/proposal-and-preview.md +++ b/design-consultation/sections/proposal-and-preview.md @@ -304,13 +304,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" ``` -Run quality check on each successful path returned by `variants`; 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 "$_DESIGN_DIR/variant-A.png" --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. @@ -321,10 +325,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 "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve +[ -f "$_DESIGN_DIR/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/`. @@ -346,18 +352,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`, `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 /tmp/gstack-design-extract-XXXXXXXX) || exit 1 @@ -373,7 +379,7 @@ _EXTRACT_DIR=$(mktemp -d /tmp/gstack-design-extract-XXXXXXXX) || exit 1 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 f76f9fcb0..42ffefefd 100644 --- a/design-consultation/sections/proposal-and-preview.md.tmpl +++ b/design-consultation/sections/proposal-and-preview.md.tmpl @@ -111,13 +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" ``` -Run quality check on each successful path returned by `variants`; 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 "$_DESIGN_DIR/variant-A.png" --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. @@ -128,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 /tmp/gstack-design-extract-XXXXXXXX) || exit 1 @@ -144,7 +148,7 @@ _EXTRACT_DIR=$(mktemp -d /tmp/gstack-design-extract-XXXXXXXX) || exit 1 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 0409f2cfa..0b9a13915 100644 --- a/design-html/SKILL.md +++ b/design-html/SKILL.md @@ -399,13 +399,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. @@ -455,8 +457,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. 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 6d97772c6..9fea4b7d0 100644 --- a/design-html/SKILL.md.tmpl +++ b/design-html/SKILL.md.tmpl @@ -82,8 +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. 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 06072d2b8..c2aa3b7ef 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -736,13 +736,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. @@ -1736,9 +1738,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. @@ -1828,7 +1833,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 d343c20ca..e3c05fd39 100644 --- a/design-shotgun/SKILL.md +++ b/design-shotgun/SKILL.md @@ -410,13 +410,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. @@ -552,7 +554,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 @@ -576,14 +578,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. @@ -674,58 +678,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: /tmp/variant-{letter}.png -Final location: {_DESIGN_DIR absolute path}/variant-{letter}.png +Design dir: {_DESIGN_DIR absolute path} -Steps: -1. Run: {$D path} generate --brief "{brief}" --output /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 /tmp/variant-{letter}.png {_DESIGN_DIR}/variant-{letter}.png -5. Quality check: {$D path} check --image {_DESIGN_DIR}/variant-{letter}.png --brief "{brief}" - If quality check fails, retry generation once. -6. Verify: ls -lh {_DESIGN_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 /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 /tmp/variant-{letter}.png +OUT=$({$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output "$STAGE/variant.png"); RC=$? ``` -**Generate to `/tmp/`, then `cp`:** in sandboxed sessions `$D generate --output` under -`~/.gstack/` can abort ("The operation was aborted"), while `/tmp/` works. +**Why staging:** in sandboxed sessions `$D generate --output` under `~/.gstack/` can +abort ("The operation was aborted"), while `/tmp/` 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 "$_DESIGN_DIR"/variant-*.png 2>/dev/null | tr '\n' ',' | sed 's/,$//') -``` - -Use `$_IMAGES` in the `$D compare --images` command. + +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 @@ -733,8 +734,12 @@ Use `$_IMAGES` in the `$D compare --images` command. 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 "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve +[ -f "$_DESIGN_DIR/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. @@ -793,8 +798,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: @@ -828,9 +833,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":"","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 @@ -854,7 +866,8 @@ Use AskUserQuestion to confirm before saving. 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 ac3d1ecdd..1845aa8c7 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. @@ -271,58 +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: /tmp/variant-{letter}.png -Final location: {_DESIGN_DIR absolute path}/variant-{letter}.png +Design dir: {_DESIGN_DIR absolute path} -Steps: -1. Run: {$D path} generate --brief "{brief}" --output /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 /tmp/variant-{letter}.png {_DESIGN_DIR}/variant-{letter}.png -5. Quality check: {$D path} check --image {_DESIGN_DIR}/variant-{letter}.png --brief "{brief}" - If quality check fails, retry generation once. -6. Verify: ls -lh {_DESIGN_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 /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 /tmp/variant-{letter}.png +OUT=$({$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output "$STAGE/variant.png"); RC=$? ``` -**Generate to `/tmp/`, then `cp`:** in sandboxed sessions `$D generate --output` under -`~/.gstack/` can abort ("The operation was aborted"), while `/tmp/` works. +**Why staging:** in sandboxed sessions `$D generate --output` under `~/.gstack/` can +abort ("The operation was aborted"), while `/tmp/` 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 "$_DESIGN_DIR"/variant-*.png 2>/dev/null | tr '\n' ',' | sed 's/,$//') -``` - -Use `$_IMAGES` in the `$D compare --images` command. + +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 @@ -349,7 +348,8 @@ Use AskUserQuestion to confirm before saving. 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/office-hours/SKILL.md b/office-hours/SKILL.md index 3e1a87541..d62225055 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -959,18 +959,26 @@ explore wide across diverse directions. **Step 3: Generate 3 variants** ```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" ``` This generates 3 style variations of the same brief (~40 seconds total). + +**Round accounting (before any board, check or inline view):** `$D` never overwrites; a taken name is bumped (for example `-2`), so use only the paths its JSON printed and carry them as literal paths into later blocks (bash blocks do not share variables). Tell the user how many of `requested` paid images were saved (`saved`) and name each `failures` entry. Route on the exit code: 0 continue with `saved`; 3 continue with `saved` and say the run stopped early; 2 nothing was saved, so report `failures` and stop: do not run `$D compare` or `$D check`. + **Step 4: Show variants inline, then open comparison board** -Show each variant to the user inline first (read the PNGs with Read tool), then +Show each saved image to the user inline first (read the printed paths with Read tool), then create and serve the comparison board: + +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 "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve +[ -f "$_DESIGN_DIR/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 the board to the design daemon, opens it in the user's default @@ -987,8 +995,8 @@ with Read and fall back to AskUserQuestion: "Which variant do you prefer? Any fe If the JSON contains `"regenerated": true`: 1. Read `regenerateAction` (or `remixSpec` for remix requests) -2. Generate new variants with `$D iterate` or `$D variants` using updated brief -3. Create new board with `$D compare` +2. Generate new variants with `$D iterate` or `$D variants` using updated brief (capture and round accounting as in Step 3) +3. Rebuild the board with Step 4's board block, without `--serve` 4. POST the new HTML to the running board. Parse the board URL from stderr (`BOARD_URL: http://127.0.0.1:N/boards//` — the daemon path) or fall back to the legacy port (`SERVE_STARTED: port=N` — only emitted under @@ -1000,11 +1008,19 @@ If `"regenerated": false`: proceed with the approved variant. **Step 6: Save approved choice** +Map the confirmed letter through this board's `board-images.json` (never the directory listing) and save it: + ```bash -echo '{"approved_variant":"","feedback":"","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"mockup","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":"mockup","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 ``` -Reference the saved mockup in the design doc or plan. +Reference the printed `APPROVED_IMAGE` in the design doc or plan. ## Visual Sketch (UI ideas only) diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index a430512e2..01a9faa0e 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -654,13 +654,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. @@ -758,6 +760,7 @@ planning phase. Generating mockups during planning is the whole point. Allowed commands under this exception: - `mkdir -p "$GSTACK_STATE_ROOT/projects/$SLUG/designs/..."` - `$D generate`, `$D variants`, `$D compare`, `$D iterate`, `$D evolve`, `$D check` +- Writing `board-images.json` and `approved.json`, and archiving `feedback.json`, inside `$_DESIGN_DIR` - `open` (viewing comparison boards in the default browser) First, set up the output directory. Name it after the screen/feature being designed and today's date: @@ -778,13 +781,17 @@ fewer variants and benefits from sequential control; /design-shotgun is the para For each UI screen/section in scope, construct a design brief from the plan's description (and DESIGN.md if present) and generate variants: ```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" ``` -After generation, run a cross-model quality check on each variant: + +**Round accounting (before any check or board):** `$D` never overwrites, so a taken name is bumped (for example `-2`); use only the printed `saved` paths. Tell the user how many of `requested` paid images were saved and name each `failures` entry. Exit 0: continue with `saved`. Exit 2: nothing was saved; report `failures` and stop: no `$D check`, no board. + +Then run a cross-model quality check on each saved path, starting with the first: ```bash -$D check --image "$_DESIGN_DIR/variant-A.png" --brief "" +$D check --image "" --brief "" ``` Flag any variants that fail the quality check. Offer to regenerate failures. @@ -798,8 +805,12 @@ feedback output. Showing mockups inline is a degraded experience. 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 "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve +[ -f "$_DESIGN_DIR/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. @@ -858,8 +869,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: @@ -893,9 +904,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":"","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 ``` **Do NOT use AskUserQuestion to ask which variant the user picked.** Read `feedback.json` — it already contains their preferred variant, ratings, comments, and overall feedback. Only use AskUserQuestion to confirm you understood the feedback correctly, never to re-ask what they chose. @@ -1105,11 +1123,11 @@ mkdir -p "$_DESIGN_DIR" _ROOT=$(git rev-parse --show-toplevel 2>/dev/null) D="$_ROOT/.claude/skills/gstack/design/dist/design" [ -x "$D" ] || D=~/.claude/skills/gstack/design/dist/design -"$D" generate --brief "" --output "$_DESIGN_DIR/ideal-.png" -echo "IDEAL_IMAGE: $_DESIGN_DIR/ideal-.png" +_OUT=$("$D" generate --brief "" --output "$_DESIGN_DIR/ideal-.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` -Show the mockup to the user via the Read tool. This makes the gap between +Exit 0: show the image at the printed `outputPath` (it may be bumped, never overwritten) to the user via the Read tool. Exit 2 or 3: report `failures` and continue text-only. This makes the gap between "what the plan describes" and "what it should look like" visceral, not abstract. If the design binary is not available, skip this and continue with text-based diff --git a/plan-design-review/SKILL.md.tmpl b/plan-design-review/SKILL.md.tmpl index 023217a56..61f9c94c4 100644 --- a/plan-design-review/SKILL.md.tmpl +++ b/plan-design-review/SKILL.md.tmpl @@ -219,6 +219,7 @@ planning phase. Generating mockups during planning is the whole point. Allowed commands under this exception: - `mkdir -p "$GSTACK_STATE_ROOT/projects/$SLUG/designs/..."` - `$D generate`, `$D variants`, `$D compare`, `$D iterate`, `$D evolve`, `$D check` +- Writing `board-images.json` and `approved.json`, and archiving `feedback.json`, inside `$_DESIGN_DIR` - `open` (viewing comparison boards in the default browser) First, set up the output directory. Name it after the screen/feature being designed and today's date: @@ -239,13 +240,17 @@ fewer variants and benefits from sequential control; /design-shotgun is the para For each UI screen/section in scope, construct a design brief from the plan's description (and DESIGN.md if present) and generate variants: ```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" ``` -After generation, run a cross-model quality check on each variant: + +**Round accounting (before any check or board):** `$D` never overwrites, so a taken name is bumped (for example `-2`); use only the printed `saved` paths. Tell the user how many of `requested` paid images were saved and name each `failures` entry. Exit 0: continue with `saved`. Exit 2: nothing was saved; report `failures` and stop: no `$D check`, no board. + +Then run a cross-model quality check on each saved path, starting with the first: ```bash -$D check --image "$_DESIGN_DIR/variant-A.png" --brief "" +$D check --image "" --brief "" ``` Flag any variants that fail the quality check. Offer to regenerate failures. @@ -303,11 +308,11 @@ mkdir -p "$_DESIGN_DIR" _ROOT=$(git rev-parse --show-toplevel 2>/dev/null) D="$_ROOT/.claude/skills/gstack/design/dist/design" [ -x "$D" ] || D=~/.claude/skills/gstack/design/dist/design -"$D" generate --brief "" --output "$_DESIGN_DIR/ideal-.png" -echo "IDEAL_IMAGE: $_DESIGN_DIR/ideal-.png" +_OUT=$("$D" generate --brief "" --output "$_DESIGN_DIR/ideal-.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` -Show the mockup to the user via the Read tool. This makes the gap between +Exit 0: show the image at the printed `outputPath` (it may be bumped, never overwritten) to the user via the Read tool. Exit 2 or 3: report `failures` and continue text-only. This makes the gap between "what the plan describes" and "what it should look like" visceral, not abstract. If the design binary is not available, skip this and continue with text-based diff --git a/plan-design-review/sections/review-sections.md b/plan-design-review/sections/review-sections.md index 67c4465bb..86525df56 100644 --- a/plan-design-review/sections/review-sections.md +++ b/plan-design-review/sections/review-sections.md @@ -216,7 +216,7 @@ FIX TO 10: Rewrite vague UI descriptions with specific alternatives: - "Hero section" → what makes this hero feel like THIS product? - "Clean, modern UI" → meaningless. Replace with actual design decisions. - "Dashboard with widgets" → what makes this NOT every other dashboard? -If visual mockups were generated in Step 0.5, evaluate them against the AI slop blacklist above. Read each mockup image using the Read tool. Does the mockup fall into generic patterns (3-column grid, centered hero, stock-photo feel)? If so, flag it and offer to regenerate with more specific direction via `$D iterate --feedback "..."`. +If visual mockups were generated in Step 0.5, evaluate them against the AI slop blacklist above. Read each mockup image using the Read tool. Does the mockup fall into generic patterns (3-column grid, centered hero, stock-photo feel)? If so, flag it and offer to regenerate with more specific direction via `$D iterate --feedback "..."`; use its printed `outputPath` for any later view, check or board. **STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. ### Pass 5: Design System Alignment @@ -257,7 +257,7 @@ If mockups were generated in Step 0.5 and review passes changed significant desi AskUserQuestion: "The review passes changed [list major design changes]. Want me to regenerate mockups to reflect the updated plan? This ensures the visual reference matches what we're actually building." -If yes, use `$D iterate` with feedback summarizing the changes, or `$D variants` with an updated brief. Save to the same `$_DESIGN_DIR` directory. +If yes, use `$D iterate` with feedback summarizing the changes, or `$D variants` with an updated brief. Save to the same `$_DESIGN_DIR` directory; nothing is overwritten, so do round accounting and carry the printed paths into any new board (rebuild it with the board block, which rewrites `board-images.json`) or check. ## How to ask questions Follow the AskUserQuestion format from the Preamble above. Additional rules for plan design reviews: diff --git a/plan-design-review/sections/review-sections.md.tmpl b/plan-design-review/sections/review-sections.md.tmpl index 32c06a75a..ee1627124 100644 --- a/plan-design-review/sections/review-sections.md.tmpl +++ b/plan-design-review/sections/review-sections.md.tmpl @@ -79,7 +79,7 @@ FIX TO 10: Rewrite vague UI descriptions with specific alternatives: - "Hero section" → what makes this hero feel like THIS product? - "Clean, modern UI" → meaningless. Replace with actual design decisions. - "Dashboard with widgets" → what makes this NOT every other dashboard? -If visual mockups were generated in Step 0.5, evaluate them against the AI slop blacklist above. Read each mockup image using the Read tool. Does the mockup fall into generic patterns (3-column grid, centered hero, stock-photo feel)? If so, flag it and offer to regenerate with more specific direction via `$D iterate --feedback "..."`. +If visual mockups were generated in Step 0.5, evaluate them against the AI slop blacklist above. Read each mockup image using the Read tool. Does the mockup fall into generic patterns (3-column grid, centered hero, stock-photo feel)? If so, flag it and offer to regenerate with more specific direction via `$D iterate --feedback "..."`; use its printed `outputPath` for any later view, check or board. **STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. ### Pass 5: Design System Alignment @@ -120,7 +120,7 @@ If mockups were generated in Step 0.5 and review passes changed significant desi AskUserQuestion: "The review passes changed [list major design changes]. Want me to regenerate mockups to reflect the updated plan? This ensures the visual reference matches what we're actually building." -If yes, use `$D iterate` with feedback summarizing the changes, or `$D variants` with an updated brief. Save to the same `$_DESIGN_DIR` directory. +If yes, use `$D iterate` with feedback summarizing the changes, or `$D variants` with an updated brief. Save to the same `$_DESIGN_DIR` directory; nothing is overwritten, so do round accounting and carry the printed paths into any new board (rebuild it with the board block, which rewrites `board-images.json`) or check. ## How to ask questions Follow the AskUserQuestion format from the Preamble above. Additional rules for plan design reviews: diff --git a/scripts/resolvers/design.ts b/scripts/resolvers/design.ts index 075aa3d82..0f74c918c 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -1114,6 +1114,33 @@ ${slopSection} Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developers.openai.com/blog/designing-delightful-frontends-with-gpt-5-4) (Mar 2026) + gstack design methodology.`; } +const DESIGN_ROUND_ACCOUNTING = ` +**Round accounting (before any board, check or inline view):** \`$D\` never overwrites; a taken name is bumped (for example \`-2\`), so use only the paths its JSON printed and carry them as literal paths into later blocks (bash blocks do not share variables). Tell the user how many of \`requested\` paid images were saved (\`saved\`) and name each \`failures\` entry. Route on the exit code: 0 continue with \`saved\`; 3 continue with \`saved\` and say the run stopped early; 2 nothing was saved, so report \`failures\` and stop: do not run \`$D compare\` or \`$D check\`.`; + +function designBoardBlock(serve: boolean): string { + return ` +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 +[ -f "$_DESIGN_DIR/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 ? ' --serve' : ''} +\`\`\``; +} + +function designApprovalBlock(screen: string): string { + return `Map the confirmed letter through this board's \`board-images.json\` (never the directory listing) and save it: + +\`\`\`bash +_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":"${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 +\`\`\``; +} + export function generateDesignSetup(ctx: TemplateContext): string { return `## DESIGN SETUP (run this check BEFORE any design mockup command) @@ -1140,12 +1167,14 @@ Comparison boards are local HTML files: open them with \`open file://...\` on ma 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${ctx.skillName === 'design-consultation' ? ` +- \`$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=$?\`.${ctx.skillName === 'design-consultation' ? ` - \`$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.` : ''} @@ -1192,19 +1221,20 @@ explore wide across diverse directions. **Step 3: Generate 3 variants** \`\`\`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" \`\`\` This generates 3 style variations of the same brief (~40 seconds total). +${DESIGN_ROUND_ACCOUNTING} + **Step 4: Show variants inline, then open comparison board** -Show each variant to the user inline first (read the PNGs with Read tool), then +Show each saved image to the user inline first (read the printed paths with Read tool), then create and serve the comparison board: -\`\`\`bash -$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve -\`\`\` +${designBoardBlock(true)} This publishes the board to the design daemon, opens it in the user's default browser, and exits; it does not wait for feedback. Read captured stderr for the @@ -1220,8 +1250,8 @@ with Read and fall back to AskUserQuestion: "Which variant do you prefer? Any fe If the JSON contains \`"regenerated": true\`: 1. Read \`regenerateAction\` (or \`remixSpec\` for remix requests) -2. Generate new variants with \`$D iterate\` or \`$D variants\` using updated brief -3. Create new board with \`$D compare\` +2. Generate new variants with \`$D iterate\` or \`$D variants\` using updated brief (capture and round accounting as in Step 3) +3. Rebuild the board with Step 4's board block, without \`--serve\` 4. POST the new HTML to the running board. Parse the board URL from stderr (\`BOARD_URL: http://127.0.0.1:N/boards//\` — the daemon path) or fall back to the legacy port (\`SERVE_STARTED: port=N\` — only emitted under @@ -1233,21 +1263,15 @@ If \`"regenerated": false\`: proceed with the approved variant. **Step 6: Save approved choice** -\`\`\`bash -echo '{"approved_variant":"","feedback":"","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"mockup","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" -\`\`\` +${designApprovalBlock('mockup')} -Reference the saved mockup in the design doc or plan.`; +Reference the printed \`APPROVED_IMAGE\` in the design doc or plan.`; } export function generateDesignShotgunLoop(ctx: TemplateContext): string { if (ctx.skillName === 'design-consultation') return `### Comparison Board + Feedback Loop -Use the successful, quality-checked paths in this example: - -\`\`\`bash -$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve -\`\`\` +${designBoardBlock(true)} 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/\`. @@ -1268,23 +1292,21 @@ 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\`, \`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.`; return `### Comparison Board + Feedback Loop Create the comparison board and serve it over HTTP: -\`\`\`bash -$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve -\`\`\` +${designBoardBlock(true)} 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. @@ -1342,8 +1364,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: @@ -1377,10 +1399,7 @@ Is this right?" Use AskUserQuestion to verify before proceeding. -**Save the approved choice:** -\`\`\`bash -echo '{"approved_variant":"","feedback":"","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" -\`\`\``; +**Save the approved choice.** ${designApprovalBlock('')}`; } export function generateTasteProfile(ctx: TemplateContext): string { @@ -1410,7 +1429,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 \`${ctx.paths.binDir}/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/test/design-approval-flow.test.ts b/test/design-approval-flow.test.ts new file mode 100644 index 000000000..ef6e740d8 --- /dev/null +++ b/test/design-approval-flow.test.ts @@ -0,0 +1,97 @@ +/** + * Stubbed two-round design flow: round two lands at bumped names, the board + * block archives round one's Submit, approval maps the letter through this + * board's board-images.json, and a fresh session (relocated directory) + * resolves the approved image. Runs the generated SKILL.md bash blocks with + * the image API stubbed (no spend). + */ + +import { afterEach, beforeEach, expect, test } from 'bun:test'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const SKILL = fs.readFileSync(path.join(ROOT, 'plan-design-review/SKILL.md'), 'utf8'); + +function bashBlockAfter(marker: string): string { + const at = SKILL.indexOf(marker); + expect(at).toBeGreaterThan(-1); + return SKILL.slice(at).match(/```bash\n([\s\S]*?)```/)![1]; +} + +const BOARD = bashBlockAfter(''); +const APPROVAL = SKILL.slice(SKILL.indexOf('**Save the approved choice.**')).match(/```bash\n([\s\S]*?)```/)![1]; + +let dir: string; +let designDir: string; +let env: Record; + +beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-approval-flow-')); + designDir = path.join(dir, 'state', 'designs', 'home-20261003'); + const preload = path.join(dir, 'stub.ts'); + fs.writeFileSync(preload, `let n = 0; +globalThis.fetch = (async () => Response.json({ id: "r", output: [{ type: "image_generation_call", result: Buffer.from("round image " + (n++)).toString("base64") }] })) as typeof fetch; +`); + const design = path.join(dir, 'design'); + fs.writeFileSync(design, `#!/bin/sh +if [ "$1" = compare ]; then printf '%s\\n' "$*" > "$COMPARE_ARGS"; exit 0; fi +exec ${JSON.stringify(process.execPath)} --no-env-file --preload ${JSON.stringify(preload)} ${JSON.stringify(path.join(ROOT, 'design/src/cli.ts'))} "$@" +`, { mode: 0o755 }); + env = { + PATH: process.env.PATH!, HOME: dir, GSTACK_HOME: path.join(dir, 'gstack'), TMPDIR: dir, + OPENAI_API_KEY: 'fixture-not-a-real-key', D: design, _DESIGN_DIR: designDir, COMPARE_ARGS: path.join(dir, 'compare-args'), + }; +}); + +afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + +const sh = (script: string) => spawnSync('bash', ['-c', script], { cwd: dir, env, encoding: 'utf8', timeout: 60_000 }); +const variants = () => JSON.parse(sh('"$D" variants --brief "home page" --count 2 --output-dir "$_DESIGN_DIR/"').stdout); +const approve = (letter: string) => sh(APPROVAL.replaceAll('', letter).replaceAll('', 'calmer').replaceAll('', 'home')); + +// Value: protects=round two's approval comes from round two's board, never round one's stale feedback.json or the newest-file listing; +// fails_when=the board block stops archiving feedback.json or approval maps letters through the directory; why_new=eng feedback-binding; seam=none +test('round two board archives round one Submit and approval maps to the bumped image', () => { + const round1 = variants(); + fs.writeFileSync(path.join(designDir, 'board-images.json'), JSON.stringify(round1.paths)); + fs.writeFileSync(path.join(designDir, 'feedback.json'), JSON.stringify({ preferred: 'B', regenerated: false })); + + const round2 = variants(); + expect(round2.paths.map((p: string) => path.basename(p))).toEqual(['variant-A-2.png', 'variant-B-2.png']); + fs.writeFileSync(path.join(designDir, 'board-images.json'), JSON.stringify(round2.paths)); + const board = sh(BOARD); + expect(board.status, board.stderr).toBe(0); + expect(fs.existsSync(path.join(designDir, 'feedback.json'))).toBe(false); + const archived = fs.readdirSync(designDir).filter(f => /^feedback-\d{8}T\d{6}Z\.json$/.test(f)); + expect(archived).toHaveLength(1); + expect(fs.readFileSync(env.COMPARE_ARGS, 'utf8')).toContain(`--images-file ${path.join(designDir, 'board-images.json')}`); + + const approved = approve('A'); + expect(approved.status, approved.stderr).toBe(0); + expect(approved.stdout).toContain(`APPROVED_IMAGE: ${path.join(designDir, 'variant-A-2.png')}`); + const record = JSON.parse(fs.readFileSync(path.join(designDir, 'approved.json'), 'utf8')); + expect(record).toMatchObject({ approved_variant: 'A', approved_path: 'variant-A-2.png', screen: 'home', feedback: 'calmer' }); + + const missing = approve('C'); + expect(missing.stdout).toContain('NO_BOARD_IMAGE'); + expect(JSON.parse(fs.readFileSync(path.join(designDir, 'approved.json'), 'utf8')).approved_path).toBe('variant-A-2.png'); +}); + +// Value: protects=a fresh session in a relocated state root opens round two's approved image, not variant-A.png; +// fails_when=approved_path is stored absolute or readers resolve by letter; why_new=DX relocation + CEO fresh-session recovery; seam=none +test('fresh session resolves the approved bumped image after relocation', () => { + variants(); + const round2 = variants(); + fs.writeFileSync(path.join(designDir, 'board-images.json'), JSON.stringify(round2.paths)); + expect(approve('A').status).toBe(0); + const moved = path.join(dir, 'moved', 'home-20261003'); + fs.cpSync(designDir, moved, { recursive: true }); + fs.rmSync(path.join(dir, 'state'), { recursive: true }); + const r = spawnSync(process.execPath, [path.join(ROOT, 'bin/gstack-design-approved'), path.join(moved, 'approved.json')], { encoding: 'utf8', timeout: 30_000 }); + expect(r.status, r.stderr).toBe(0); + expect(r.stdout.trim()).toBe(path.join(moved, 'variant-A-2.png')); + expect(fs.readFileSync(r.stdout.trim(), 'utf8')).toBe('round image 0'); +}); diff --git a/test/design-printed-paths.test.ts b/test/design-printed-paths.test.ts new file mode 100644 index 000000000..aa16fb8fa --- /dev/null +++ b/test/design-printed-paths.test.ts @@ -0,0 +1,165 @@ +/** + * Tripwire: design skills read the paths `$D` printed for this round, never a + * fixed variant name or a directory glob, because `$D` never overwrites and a + * second round lands at bumped names (variant-A-2.png). Runs on the generated + * SKILL.md files (with their sections) and the resolver sources. + */ + +import { describe, expect, test } from 'bun:test'; +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(import.meta.dir, '..'); + +/** The one reasoned exception, anchored on content rather than a line number. */ +const ALLOWLIST = [{ + file: 'design-html/SKILL.md', + line: '_VARIANTS=$(ls -t "$GSTACK_STATE_ROOT/projects/$SLUG/designs/"*/variant-*.png 2>/dev/null | head -1)', + reason: 'cross-session discovery probe: design-html runs no generator, it only detects that earlier rounds exist; its approved image comes from approved.json', +}]; + +const GENERATOR = /(?:\$D|"\$D")\s+(generate|variants|iterate|evolve)\b/; +const CONSUMER = /^\s*(?:\$D|"\$D")\s+(compare|check)\b/m; + +function flowFiles(): string[] { + const out: string[] = []; + for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue; + const skill = path.join(entry.name, 'SKILL.md'); + if (fs.existsSync(path.join(ROOT, skill))) out.push(skill); + const sections = path.join(entry.name, 'sections'); + if (fs.existsSync(path.join(ROOT, sections))) { + for (const f of fs.readdirSync(path.join(ROOT, sections))) if (f.endsWith('.md')) out.push(path.join(sections, f)); + } + } + for (const f of fs.readdirSync(path.join(ROOT, 'scripts/resolvers'))) if (f.endsWith('.ts')) out.push(path.join('scripts/resolvers', f)); + return out; +} + +function bashLines(text: string): string[] { + return [...text.matchAll(/```bash\n([\s\S]*?)```/g)].flatMap(m => m[1].split('\n')); +} + +function argAfter(line: string, flag: string): string | null { + const m = line.match(new RegExp(`${flag}\\s+("([^"]+)"|(\\S+))`)); + return m ? (m[2] ?? m[3]) : null; +} + +/** Concrete bans only; returns one message per violation. */ +export function scanForBannedReads(file: string, text: string): string[] { + const violations: string[] = []; + const allowed = new Set(ALLOWLIST.filter(a => a.file === file).map(a => a.line.trim())); + const outputs = new Set(); + for (const line of text.split('\n')) { + if (GENERATOR.test(line)) { + const out = argAfter(line, '--output'); + if (out) outputs.add(out); + } + } + text.split('\n').forEach((line, i) => { + const where = `${file}:${i + 1}`; + if (allowed.has(line.trim())) return; + if (/variant-[A-Z]\.png/.test(line)) violations.push(`${where} fixed variant name: ${line.trim()}`); + if (/variant-\*\.png/.test(line)) violations.push(`${where} variant glob: ${line.trim()}`); + if (/variant-\{letter\}/.test(line) && !/gstack-design-claim/.test(line)) violations.push(`${where} variant-{letter} placeholder read: ${line.trim()}`); + const copy = line.match(/(?:^|[\s;&|(`])(?:cp|mv)\s+(.*)$/); + if (copy && (/variant|responsive-|outputPath/.test(copy[1]) || [...outputs].some(o => copy[1].includes(o)))) { + violations.push(`${where} cp/mv of a $D output (publish with gstack-design-claim): ${line.trim()}`); + } + const image = /\$D\s+check\b/.test(line) ? argAfter(line, '--image') : /\$D\s+verify\b/.test(line) ? argAfter(line, '--mockup') : null; + if (image && outputs.has(image)) violations.push(`${where} reads the requested --output instead of the printed path: ${line.trim()}`); + }); + return violations; +} + +const files = flowFiles(); +const read = (f: string) => fs.readFileSync(path.join(ROOT, f), 'utf8'); + +describe('design printed-paths tripwire', () => { + // Value: protects=no skill reads a fixed variant name, glob or requested --output after $D bumps a name; + // fails_when=a template reintroduces variant-A.png, variant-*.png, a cp of a $D output or a literal check path; why_new=#1529; seam=none + test('generated skills and resolvers read only printed paths', () => { + const violations = files.flatMap(f => scanForBannedReads(f, read(f))); + expect(violations).toEqual([]); + }); + + // Value: protects=the allowlist cannot rot into a blanket pass; fails_when=the probe line changes and the entry no longer anchors; + // why_new=content-anchored allowlist; seam=none + test('the allowlist entry still anchors to real content', () => { + for (const a of ALLOWLIST) { + expect(read(a.file).split('\n').map(l => l.trim())).toContain(a.line); + expect(a.reason.length).toBeGreaterThan(20); + } + }); + + // Value: protects=the scanner actually catches each banned form; fails_when=a regex is loosened until it matches nothing; + // why_new=negative controls for the tripwire; seam=none + test('fixtures with each banned form fail', () => { + const cases = [ + '$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png"', + '_IMAGES=$(ls "$_DESIGN_DIR"/variant-*.png)', + '5. Quality check: {$D path} check --image {_DESIGN_DIR}/variant-{letter}.png', + 'cp /tmp/variant-B.png "$_DESIGN_DIR/"', + '$D generate --brief "x" --output "$DIR/target.png"\n$D verify --mockup "$DIR/target.png" --screenshot after.png', + '$D generate --brief "x" --output "$DIR/target.png"\n$D check --image "$DIR/target.png" --brief x', + ]; + for (const c of cases) expect(scanForBannedReads('fixture.md', c).length, c).toBeGreaterThan(0); + expect(scanForBannedReads('fixture.md', 'FINAL=$(gstack-design-claim "" "{dir}/variant-{letter}.png")')).toEqual([]); + }); +}); + +describe('design generating flows', () => { + const flows: Record = { + 'office-hours': ['office-hours/SKILL.md'], + 'plan-design-review': ['plan-design-review/SKILL.md'], + 'design-consultation': ['design-consultation/sections/proposal-and-preview.md'], + 'design-shotgun': ['design-shotgun/SKILL.md'], + }; + + for (const [skill, sources] of Object.entries(flows)) { + // Value: protects=round accounting (with the zero-saved stop) comes before the first board or check in each flow; + // fails_when=a template moves the board or check above the accounting marker or drops the stop; why_new=CEO accounting position; seam=none + test(`${skill}: round accounting precedes the first consumer and stops on zero saved`, () => { + const text = sources.map(read).join('\n'); + const accounting = text.indexOf(''); + const board = text.indexOf(''); + const consumer = text.search(CONSUMER); + expect(accounting).toBeGreaterThan(-1); + expect(board).toBeGreaterThan(accounting); + expect(consumer).toBeGreaterThan(accounting); + const span = text.slice(accounting, Math.min(board, consumer)); + expect(span).toMatch(/stop/i); + expect(span).toMatch(/nothing was saved|saves nothing|zero/i); + }); + } + + // Value: protects=generating steps capture JSON and the exit code instead of dying under set -e; + // fails_when=a bare `$D generate ...` line returns; why_new=eng template exit handling; seam=none + test('every generating bash line captures stdout and the exit code', () => { + const bare = files.filter(f => f.endsWith('.md')).flatMap(f => bashLines(read(f)) + .filter(l => GENERATOR.test(l) && !/=\$\(.*\);\s*_?RC=\$\?/.test(l)) + .map(l => `${f}: ${l.trim()}`)); + expect(bare).toEqual([]); + }); + + // Value: protects=every approval writer records approved_path so a bumped round resolves to the right image; + // fails_when=a writer goes back to approved_variant only; why_new=approved-image identity; seam=none + test('every approval-record writer stores approved_path', () => { + const writers = files.flatMap(f => read(f).split('\n') + .filter(l => /approved_variant/.test(l) && (/>\s*"\$_DESIGN_DIR\/approved\.json"/.test(l) || /Write of `\$_DESIGN_DIR\/approved\.json`/.test(l))) + .map(l => ({ f, l }))); + expect(writers.length).toBeGreaterThanOrEqual(3); + for (const { f, l } of writers) expect(l, f).toContain('approved_path'); + }); + + // Value: protects=shotgun subagents stage in a fresh mktemp dir and publish through the no-clobber helper; + // fails_when=the shared /tmp/variant file and cp return; why_new=eng staging requirement; seam=none + test('design-shotgun stages per run and publishes with gstack-design-claim', () => { + const shotgun = read('design-shotgun/SKILL.md'); + const prompt = shotgun.slice(shotgun.indexOf('**Agent prompt template**'), shotgun.indexOf('### Step 3d')); + expect(prompt).toContain('mktemp -d /tmp/'); + expect(prompt).toMatch(/gstack-design-claim ""/); + expect(prompt).not.toMatch(/\/tmp\/variant-/); + expect(prompt).toMatch(/every published path/); + }); +});