From 7637f604a4bdebc7304581a3b985af59dcf4e570 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 6 Oct 2026 20:55:52 +0000 Subject: [PATCH] fix: design briefs and approval feedback travel as agent-written files Design briefs went into $D variants/generate/check as a double-quoted argument, and approval feedback was spliced into a single-quoted echo of JSON, so a quote, backtick or $(...) in either broke the command or ran. The agent now writes each brief into a private .gstack/tmp file and the block passes --brief "$(cat "$BRIEF_FILE")" (the CLI's --brief-file expects a JSON brief, so plain text uses --brief). variants and the later check reuse the same brief file. approved.json is built with jq -n from --arg values and --rawfile feedback, so the feedback is data. The office-hours mockup flow creates its brief and feedback files in one block. design-approval-flow now writes the feedback file (with quotes, backticks and $(...) in it) instead of substituting , and checks the file is removed after the save. Co-authored-by: dgrant --- design-consultation/SKILL.md | 8 +-- .../sections/proposal-and-preview.md | 25 +++++-- .../sections/proposal-and-preview.md.tmpl | 16 +++-- design-html/SKILL.md | 8 +-- design-review/SKILL.md | 27 ++++++-- design-review/SKILL.md.tmpl | 12 +++- design-shotgun/SKILL.md | 23 +++++-- office-hours/SKILL.md | 28 ++++++-- plan-design-review/SKILL.md | 66 +++++++++++++++---- plan-design-review/SKILL.md.tmpl | 29 ++++++-- scripts/resolvers/design.ts | 48 ++++++++++---- test/design-approval-flow.test.ts | 13 +++- test/helpers/placeholder-allowlist.ts | 2 + 13 files changed, 228 insertions(+), 77 deletions(-) diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index bae0d3581..e23cb4016 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -561,14 +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 (prints `outputPath`) -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D generate --brief "$(cat "$BRIEF_FILE")" --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "$(cat "$BRIEF_FILE")" --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 check --image /path.png --brief "$(cat "$BRIEF_FILE")"` — 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=$?`. +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=$?`. Briefs travel as files, never inline. - `$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. diff --git a/design-consultation/sections/proposal-and-preview.md b/design-consultation/sections/proposal-and-preview.md index fa1546e25..9945aab1e 100644 --- a/design-consultation/sections/proposal-and-preview.md +++ b/design-consultation/sections/proposal-and-preview.md @@ -303,20 +303,35 @@ mkdir -p "$_DESIGN_DIR" echo "DESIGN_DIR: $_DESIGN_DIR" ``` -Brief: Phase 3 aesthetic/colors/type/spacing/layout plus Phase 1 product context: +Brief: Phase 3 aesthetic/colors/type/spacing/layout plus Phase 1 product context, written into a private file: ```bash -_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" +mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } +_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } +BRIEF_FILE=$(mktemp "$_GT/brief.XXXXXX") && echo "BRIEF_FILE: $BRIEF_FILE (name: ${BRIEF_FILE##*/})" +``` + +Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. + +Brief shape: `Product name: [name]. Product type: [type]. Aesthetic: [direction]. Colors: primary [hex], secondary [hex], neutrals [range]. Typography: display [font], body [font]. Layout: [approach]. Show a realistic [page type] screen with [specific content for this product].` Then substitute the printed name for ``: + +```bash +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +_OUT=$($D variants --brief "$(cat "$BRIEF_FILE")" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` **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: +Run quality check on each saved path, starting with the first, against the same brief file; never include failed variants: ```bash -$D check --image "" --brief "" +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +$D check --image "" --brief "$(cat "$BRIEF_FILE")" ``` 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. @@ -354,7 +369,7 @@ 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 (the board block does this on rebuild). +1. Revise the brief in the brief file, 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): diff --git a/design-consultation/sections/proposal-and-preview.md.tmpl b/design-consultation/sections/proposal-and-preview.md.tmpl index 9328f6b44..9bc4247b0 100644 --- a/design-consultation/sections/proposal-and-preview.md.tmpl +++ b/design-consultation/sections/proposal-and-preview.md.tmpl @@ -108,20 +108,28 @@ mkdir -p "$_DESIGN_DIR" echo "DESIGN_DIR: $_DESIGN_DIR" ``` -Brief: Phase 3 aesthetic/colors/type/spacing/layout plus Phase 1 product context: +Brief: Phase 3 aesthetic/colors/type/spacing/layout plus Phase 1 product context, written into a private file: + +{{FREE_TEXT_FILE:BRIEF_FILE=brief}} + +Brief shape: `Product name: [name]. Product type: [type]. Aesthetic: [direction]. Colors: primary [hex], secondary [hex], neutrals [range]. Typography: display [font], body [font]. Layout: [approach]. Show a realistic [page type] screen with [specific content for this product].` Then substitute the printed name for ``: ```bash -_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +_OUT=$($D variants --brief "$(cat "$BRIEF_FILE")" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` **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: +Run quality check on each saved path, starting with the first, against the same brief file; never include failed variants: ```bash -$D check --image "" --brief "" +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +$D check --image "" --brief "$(cat "$BRIEF_FILE")" ``` 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. diff --git a/design-html/SKILL.md b/design-html/SKILL.md index e9ec85a16..e251949d3 100644 --- a/design-html/SKILL.md +++ b/design-html/SKILL.md @@ -409,14 +409,14 @@ 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 (prints `outputPath`) -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D generate --brief "$(cat "$BRIEF_FILE")" --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "$(cat "$BRIEF_FILE")" --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 check --image /path.png --brief "$(cat "$BRIEF_FILE")"` — 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=$?`. +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=$?`. Briefs travel as files, never inline. **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 diff --git a/design-review/SKILL.md b/design-review/SKILL.md index b076ca7af..648fbcf13 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -747,14 +747,14 @@ 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 (prints `outputPath`) -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D generate --brief "$(cat "$BRIEF_FILE")" --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "$(cat "$BRIEF_FILE")" --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 check --image /path.png --brief "$(cat "$BRIEF_FILE")"` — 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=$?`. +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=$?`. Briefs travel as files, never inline. **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 @@ -1749,11 +1749,24 @@ For each fixable finding, in impact order: ### 8a.5. Target Mockup (if DESIGN_READY) -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: +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. Write the description of the page or component with the finding fixed, referencing DESIGN.md constraints, into a private file: ```bash -_OUT=$($D generate --brief "" --output "$REPORT_DIR/screenshots/finding-NNN-target.png"); _RC=$? -printf '%s\n' "$_OUT"; echo "EXIT: $_RC" +_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" +mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } +_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } +BRIEF_FILE=$(mktemp "$_GT/brief.XXXXXX") && echo "BRIEF_FILE: $BRIEF_FILE (name: ${BRIEF_FILE##*/})" +``` + +Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. + +Then substitute the printed name for ``: + +```bash +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +_OUT=$($D generate --brief "$(cat "$BRIEF_FILE")" --output "$REPORT_DIR/screenshots/finding-NNN-target.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC"; [ "$_RC" != 0 ] || rm -f "$BRIEF_FILE" ``` 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. diff --git a/design-review/SKILL.md.tmpl b/design-review/SKILL.md.tmpl index 5b01fd0f0..896e8cc17 100644 --- a/design-review/SKILL.md.tmpl +++ b/design-review/SKILL.md.tmpl @@ -176,11 +176,17 @@ For each fixable finding, in impact order: ### 8a.5. Target Mockup (if DESIGN_READY) -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: +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. Write the description of the page or component with the finding fixed, referencing DESIGN.md constraints, into a private file: + +{{FREE_TEXT_FILE:BRIEF_FILE=brief}} + +Then substitute the printed name for ``: ```bash -_OUT=$($D generate --brief "" --output "$REPORT_DIR/screenshots/finding-NNN-target.png"); _RC=$? -printf '%s\n' "$_OUT"; echo "EXIT: $_RC" +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +_OUT=$($D generate --brief "$(cat "$BRIEF_FILE")" --output "$REPORT_DIR/screenshots/finding-NNN-target.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC"; [ "$_RC" != 0 ] || rm -f "$BRIEF_FILE" ``` 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. diff --git a/design-shotgun/SKILL.md b/design-shotgun/SKILL.md index edf03dce2..4515ff705 100644 --- a/design-shotgun/SKILL.md +++ b/design-shotgun/SKILL.md @@ -419,14 +419,14 @@ 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 (prints `outputPath`) -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D generate --brief "$(cat "$BRIEF_FILE")" --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "$(cat "$BRIEF_FILE")" --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 check --image /path.png --brief "$(cat "$BRIEF_FILE")"` — 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=$?`. +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=$?`. Briefs travel as files, never inline. **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 @@ -836,12 +836,23 @@ Is this right?" Use AskUserQuestion to verify before proceeding. -**Save the approved choice.** Map the confirmed letter through this board's `board-images.json` (never the directory listing) and save it: +**Save the approved choice.** Write the user's feedback (`none` when they gave none) into a private file: + +```bash +_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" +mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } +_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } +FEEDBACK_FILE=$(mktemp "$_GT/feedback.XXXXXX") && echo "FEEDBACK_FILE: $FEEDBACK_FILE (name: ${FEEDBACK_FILE##*/})" +``` + +Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. Then map the confirmed letter through this board's `board-images.json` (never the directory listing) and save it, substituting the printed name for ``: ```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":"","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" + FEEDBACK_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" + [ -s "$FEEDBACK_FILE" ] || { echo "Not saved: $FEEDBACK_FILE is missing or empty. Write the feedback, then rerun this block." >&2; exit 1; } + jq -n --arg approved_variant "" --arg approved_path "$(basename "$_IMG")" --rawfile feedback "$FEEDBACK_FILE" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg screen "" --arg branch "$(git branch --show-current 2>/dev/null)" '$ARGS.named | .feedback |= rtrimstr("\n")' > "$_DESIGN_DIR/approved.json" && rm -f "$FEEDBACK_FILE" echo "APPROVED_IMAGE: $_IMG" else echo "NO_BOARD_IMAGE: is not on this board; reselect from the board" diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index 85cf7f0a5..130d43b6c 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -995,12 +995,24 @@ echo "DESIGN_DIR: $_DESIGN_DIR" **Step 2: Construct the design brief** Read DESIGN.md if it exists — use it to constrain the visual style. If no DESIGN.md, -explore wide across diverse directions. - -**Step 3: Generate 3 variants** +explore wide across diverse directions. The brief, and the user's feedback in Step 6, go in private files: ```bash -_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" +mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } +_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } +BRIEF_FILE=$(mktemp "$_GT/brief.XXXXXX") && echo "BRIEF_FILE: $BRIEF_FILE (name: ${BRIEF_FILE##*/})" +FEEDBACK_FILE=$(mktemp "$_GT/feedback.XXXXXX") && echo "FEEDBACK_FILE: $FEEDBACK_FILE (name: ${FEEDBACK_FILE##*/})" +``` + +Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. + +**Step 3: Generate 3 variants** (substitute the printed name for ``) + +```bash +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +_OUT=$($D variants --brief "$(cat "$BRIEF_FILE")" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` @@ -1036,7 +1048,7 @@ 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 (capture and round accounting as in Step 3) +2. Generate new variants with `$D iterate` or `$D variants` using the updated brief, rewritten into the brief file (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 @@ -1049,12 +1061,14 @@ 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: +Write the user's feedback (`none` when they gave none) into the feedback file from Step 2, then map the confirmed letter through this board's `board-images.json` (never the directory listing) and save it, substituting the printed name for ``: ```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":"mockup","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" + FEEDBACK_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" + [ -s "$FEEDBACK_FILE" ] || { echo "Not saved: $FEEDBACK_FILE is missing or empty. Write the feedback, then rerun this block." >&2; exit 1; } + jq -n --arg approved_variant "" --arg approved_path "$(basename "$_IMG")" --rawfile feedback "$FEEDBACK_FILE" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg screen "mockup" --arg branch "$(git branch --show-current 2>/dev/null)" '$ARGS.named | .feedback |= rtrimstr("\n")' > "$_DESIGN_DIR/approved.json" && rm -f "$FEEDBACK_FILE" echo "APPROVED_IMAGE: $_IMG" else echo "NO_BOARD_IMAGE: is not on this board; reselect from the board" diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index a212a8de5..aa5830d9c 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -664,14 +664,14 @@ 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 (prints `outputPath`) -- `$D variants --brief "..." --count 3 --output-dir /path/` — generate N style variants (prints `paths`) +- `$D generate --brief "$(cat "$BRIEF_FILE")" --output /path.png` — generate a single mockup (prints `outputPath`) +- `$D variants --brief "$(cat "$BRIEF_FILE")" --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 check --image /path.png --brief "$(cat "$BRIEF_FILE")"` — 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=$?`. +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=$?`. Briefs travel as files, never inline. **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 @@ -788,20 +788,35 @@ Replace `` with a descriptive kebab-case name (e.g., `homepage-vari **Generate mockups one screen at a time in this skill.** The inline review flow generates fewer variants and benefits from sequential control; /design-shotgun is the parallel flow. -For each UI screen/section in scope, construct a design brief from the plan's description (and DESIGN.md if present) and generate variants: +For each UI screen/section in scope, construct a design brief from the plan's description (and DESIGN.md if present) and write it into a private file: ```bash -_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" +mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } +_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } +BRIEF_FILE=$(mktemp "$_GT/brief.XXXXXX") && echo "BRIEF_FILE: $BRIEF_FILE (name: ${BRIEF_FILE##*/})" +``` + +Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. + +Then generate variants, substituting the printed name for ``: + +```bash +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +_OUT=$($D variants --brief "$(cat "$BRIEF_FILE")" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` **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: +Then run a cross-model quality check on each saved path, starting with the first, against the same brief file: ```bash -$D check --image "" --brief "" +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +$D check --image "" --brief "$(cat "$BRIEF_FILE")" ``` Flag any variants that fail the quality check. Offer to regenerate failures. @@ -914,12 +929,23 @@ Is this right?" Use AskUserQuestion to verify before proceeding. -**Save the approved choice.** Map the confirmed letter through this board's `board-images.json` (never the directory listing) and save it: +**Save the approved choice.** Write the user's feedback (`none` when they gave none) into a private file: + +```bash +_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" +mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } +_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } +FEEDBACK_FILE=$(mktemp "$_GT/feedback.XXXXXX") && echo "FEEDBACK_FILE: $FEEDBACK_FILE (name: ${FEEDBACK_FILE##*/})" +``` + +Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. Then map the confirmed letter through this board's `board-images.json` (never the directory listing) and save it, substituting the printed name for ``: ```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":"","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" + FEEDBACK_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" + [ -s "$FEEDBACK_FILE" ] || { echo "Not saved: $FEEDBACK_FILE is missing or empty. Write the feedback, then rerun this block." >&2; exit 1; } + jq -n --arg approved_variant "" --arg approved_path "$(basename "$_IMG")" --rawfile feedback "$FEEDBACK_FILE" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg screen "" --arg branch "$(git branch --show-current 2>/dev/null)" '$ARGS.named | .feedback |= rtrimstr("\n")' > "$_DESIGN_DIR/approved.json" && rm -f "$FEEDBACK_FILE" echo "APPROVED_IMAGE: $_IMG" else echo "NO_BOARD_IMAGE: is not on this board; reselect from the board" @@ -1125,9 +1151,23 @@ Re-run loop: invoke /plan-design-review again → re-rate → sections at 8+ get ### "Show me what 10/10 looks like" (requires design binary) If `DESIGN_READY` was printed during setup AND a dimension rates below 7/10, -offer to generate a visual mockup showing what the improved version would look like: +offer to generate a visual mockup showing what the improved version would look like. +Write the description of what 10/10 looks like for this dimension into a private file: ```bash +_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" +mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } +_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } +BRIEF_FILE=$(mktemp "$_GT/brief.XXXXXX") && echo "BRIEF_FILE: $BRIEF_FILE (name: ${BRIEF_FILE##*/})" +``` + +Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. + +Then substitute the printed name for ``: + +```bash +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } SLUG=$(~/.claude/skills/gstack/bin/gstack-slug --get SLUG 2>/dev/null) GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" _DESIGN_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/designs/ideal-$(date +%Y%m%d)" @@ -1135,8 +1175,8 @@ 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 -_OUT=$("$D" generate --brief "" --output "$_DESIGN_DIR/ideal-.png"); _RC=$? -printf '%s\n' "$_OUT"; echo "EXIT: $_RC" +_OUT=$("$D" generate --brief "$(cat "$BRIEF_FILE")" --output "$_DESIGN_DIR/ideal-.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC"; [ "$_RC" != 0 ] || rm -f "$BRIEF_FILE" ``` 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 diff --git a/plan-design-review/SKILL.md.tmpl b/plan-design-review/SKILL.md.tmpl index 61f9c94c4..c30f8d0a5 100644 --- a/plan-design-review/SKILL.md.tmpl +++ b/plan-design-review/SKILL.md.tmpl @@ -237,20 +237,28 @@ Replace `` with a descriptive kebab-case name (e.g., `homepage-vari **Generate mockups one screen at a time in this skill.** The inline review flow generates fewer variants and benefits from sequential control; /design-shotgun is the parallel flow. -For each UI screen/section in scope, construct a design brief from the plan's description (and DESIGN.md if present) and generate variants: +For each UI screen/section in scope, construct a design brief from the plan's description (and DESIGN.md if present) and write it into a private file: + +{{FREE_TEXT_FILE:BRIEF_FILE=brief}} + +Then generate variants, substituting the printed name for ``: ```bash -_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +_OUT=$($D variants --brief "$(cat "$BRIEF_FILE")" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? printf '%s\n' "$_OUT"; echo "EXIT: $_RC" ``` **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: +Then run a cross-model quality check on each saved path, starting with the first, against the same brief file: ```bash -$D check --image "" --brief "" +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } +$D check --image "" --brief "$(cat "$BRIEF_FILE")" ``` Flag any variants that fail the quality check. Offer to regenerate failures. @@ -298,9 +306,16 @@ Re-run loop: invoke /plan-design-review again → re-rate → sections at 8+ get ### "Show me what 10/10 looks like" (requires design binary) If `DESIGN_READY` was printed during setup AND a dimension rates below 7/10, -offer to generate a visual mockup showing what the improved version would look like: +offer to generate a visual mockup showing what the improved version would look like. +Write the description of what 10/10 looks like for this dimension into a private file: + +{{FREE_TEXT_FILE:BRIEF_FILE=brief}} + +Then substitute the printed name for ``: ```bash +BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; } SLUG=$(~/.claude/skills/gstack/bin/gstack-slug --get SLUG 2>/dev/null) GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" _DESIGN_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/designs/ideal-$(date +%Y%m%d)" @@ -308,8 +323,8 @@ 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 -_OUT=$("$D" generate --brief "" --output "$_DESIGN_DIR/ideal-.png"); _RC=$? -printf '%s\n' "$_OUT"; echo "EXIT: $_RC" +_OUT=$("$D" generate --brief "$(cat "$BRIEF_FILE")" --output "$_DESIGN_DIR/ideal-.png"); _RC=$? +printf '%s\n' "$_OUT"; echo "EXIT: $_RC"; [ "$_RC" != 0 ] || rm -f "$BRIEF_FILE" ``` 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 diff --git a/scripts/resolvers/design.ts b/scripts/resolvers/design.ts index 723920b21..b6950a831 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -1,5 +1,6 @@ import { outsideVoiceFailurePolicy, outsideVoiceFor, outsideVoiceInvocation, outsideVoicePreflight, outsideVoiceProvenance } from './outside-voice'; import { type TemplateContext, quoteSafePath, toShellPath } from './types'; +import { FREE_TEXT_WRITE_RULE, freeTextFileBash } from './free-text-file'; import { binaryAssignment } from './runtime-root'; import { usesLazySections } from './sections'; import { AI_SLOP_BLACKLIST, OPENAI_HARD_REJECTIONS, OPENAI_LITMUS_CHECKS, CC_BACKGROUND_DEFAULT_SINCE } from './constants'; @@ -1144,6 +1145,9 @@ case "$_RC" in esac`; } +const DESIGN_BRIEF_FILE_CHECK = `BRIEF_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" +[ -s "$BRIEF_FILE" ] || { echo "Not run: $BRIEF_FILE is missing or empty. Write the brief, then rerun this block." >&2; exit 1; }`; + 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\`.`; @@ -1157,13 +1161,22 @@ $D compare --images-file "$_DESIGN_DIR/board-images.json" --output "$_DESIGN_DIR \`\`\``; } -function designApprovalBlock(screen: string): string { - return `Map the confirmed letter through this board's \`board-images.json\` (never the directory listing) and save it: +/** Feedback goes into FEEDBACK_FILE; the mockup flow creates that file with its brief file in Step 2. */ +function designApprovalBlock(screen: string, createFeedbackFile = true): string { + return `${createFeedbackFile ? `Write the user's feedback (\`none\` when they gave none) into a private file: + +\`\`\`bash +${freeTextFileBash([{ variable: 'FEEDBACK_FILE', stem: 'feedback' }])} +\`\`\` + +${FREE_TEXT_WRITE_RULE} Then map` : `Write the user's feedback (\`none\` when they gave none) into the feedback file from Step 2, then map`} the confirmed letter through this board's \`board-images.json\` (never the directory listing) and save it, substituting the printed name for \`\`: \`\`\`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" + FEEDBACK_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" + [ -s "$FEEDBACK_FILE" ] || { echo "Not saved: $FEEDBACK_FILE is missing or empty. Write the feedback, then rerun this block." >&2; exit 1; } + jq -n --arg approved_variant "" --arg approved_path "$(basename "$_IMG")" --rawfile feedback "$FEEDBACK_FILE" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg screen "${screen}" --arg branch "$(git branch --show-current 2>/dev/null)" '$ARGS.named | .feedback |= rtrimstr("\\n")' > "$_DESIGN_DIR/approved.json" && rm -f "$FEEDBACK_FILE" echo "APPROVED_IMAGE: $_IMG" else echo "NO_BOARD_IMAGE: is not on this board; reselect from the board" @@ -1190,14 +1203,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 (prints \`outputPath\`) -- \`$D variants --brief "..." --count 3 --output-dir /path/\` — generate N style variants (prints \`paths\`) +- \`$D generate --brief "$(cat "$BRIEF_FILE")" --output /path.png\` — generate a single mockup (prints \`outputPath\`) +- \`$D variants --brief "$(cat "$BRIEF_FILE")" --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 check --image /path.png --brief "$(cat "$BRIEF_FILE")"\` — 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=$?\`.${ctx.skillName === 'design-consultation' ? ` +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=$?\`. Briefs travel as files, never inline.${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.` : ''} @@ -1236,12 +1249,19 @@ echo "DESIGN_DIR: $_DESIGN_DIR" **Step 2: Construct the design brief** Read DESIGN.md if it exists — use it to constrain the visual style. If no DESIGN.md, -explore wide across diverse directions. - -**Step 3: Generate 3 variants** +explore wide across diverse directions. The brief, and the user's feedback in Step 6, go in private files: \`\`\`bash -_OUT=$($D variants --brief "" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? +${freeTextFileBash([{ variable: 'BRIEF_FILE', stem: 'brief' }, { variable: 'FEEDBACK_FILE', stem: 'feedback' }])} +\`\`\` + +${FREE_TEXT_WRITE_RULE} + +**Step 3: Generate 3 variants** (substitute the printed name for \`\`) + +\`\`\`bash +${DESIGN_BRIEF_FILE_CHECK} +_OUT=$($D variants --brief "$(cat "$BRIEF_FILE")" --count 3 --output-dir "$_DESIGN_DIR/"); _RC=$? printf '%s\\n' "$_OUT"; echo "EXIT: $_RC" \`\`\` @@ -1270,7 +1290,7 @@ 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 (capture and round accounting as in Step 3) +2. Generate new variants with \`$D iterate\` or \`$D variants\` using the updated brief, rewritten into the brief file (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 @@ -1283,7 +1303,7 @@ If \`"regenerated": false\`: proceed with the approved variant. **Step 6: Save approved choice** -${designApprovalBlock('mockup')} +${designApprovalBlock('mockup', false)} Reference the printed \`APPROVED_IMAGE\` in the design doc or plan.`; } @@ -1312,7 +1332,7 @@ 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 (the board block does this on rebuild). +1. Revise the brief in the brief file, 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): diff --git a/test/design-approval-flow.test.ts b/test/design-approval-flow.test.ts index ef6e740d8..c531c1a08 100644 --- a/test/design-approval-flow.test.ts +++ b/test/design-approval-flow.test.ts @@ -22,7 +22,8 @@ function bashBlockAfter(marker: string): string { } const BOARD = bashBlockAfter(''); -const APPROVAL = SKILL.slice(SKILL.indexOf('**Save the approved choice.**')).match(/```bash\n([\s\S]*?)```/)![1]; +// The save block follows the feedback-file block. +const APPROVAL = SKILL.slice(SKILL.indexOf('**Save the approved choice.**')).match(/```bash\n(_IMG=[\s\S]*?)```/)![1]; let dir: string; let designDir: string; @@ -50,7 +51,12 @@ 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')); +// CEO-12: the feedback travels in an agent-written file under the project's .gstack/tmp, never in the command. +const approve = (letter: string) => { + fs.mkdirSync(path.join(dir, '.gstack', 'tmp'), { recursive: true }); + fs.writeFileSync(path.join(dir, '.gstack', 'tmp', 'feedback.fixture'), 'calmer "quoted" `not run` $(not run)\n'); + return sh(APPROVAL.replaceAll('', letter).replaceAll('', 'feedback.fixture').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 @@ -73,7 +79,8 @@ test('round two board archives round one Submit and approval maps to the bumped 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' }); + expect(record).toMatchObject({ approved_variant: 'A', approved_path: 'variant-A-2.png', screen: 'home', feedback: 'calmer "quoted" `not run` $(not run)' }); + expect(fs.existsSync(path.join(dir, '.gstack', 'tmp', 'feedback.fixture'))).toBe(false); const missing = approve('C'); expect(missing.stdout).toContain('NO_BOARD_IMAGE'); diff --git a/test/helpers/placeholder-allowlist.ts b/test/helpers/placeholder-allowlist.ts index 9920a7d51..3b6ad27d9 100644 --- a/test/helpers/placeholder-allowlist.ts +++ b/test/helpers/placeholder-allowlist.ts @@ -161,4 +161,6 @@ export const IDENTIFIER_PLACEHOLDERS: Record = { // A3 group 1 (Aside prompts, design briefs, design approval feedback): mktemp basenames. '': { grammar: ID, what: 'basename of the mktemp Aside prompt file printed by the previous block' }, + '': { grammar: ID, what: 'basename of the mktemp design brief file printed by the free-text block' }, + '': { grammar: ID, what: 'basename of the mktemp design feedback file printed by the previous block' }, };