From 8ed5350d08ee9597e29aff918634af03df3ed7b7 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Fri, 30 Oct 2026 11:11:03 -0400 Subject: [PATCH 01/15] docs: write the Codex image-generation recipe (E3.4.1, #65) Add a documented Codex recipe to references/art-direction.md: the codex exec command for content icon prompt output, how to read the image path from the run output (never the newest file), and the retry and attempt limits. Link it from references/content.md and note it in the changelog. Doc and test only; no behaviour change. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 + references/art-direction.md | 31 +++++++++++++++ references/content.md | 2 +- tests/test_art_direction_recipe.py | 63 ++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/test_art_direction_recipe.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cd5741..740f93e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon check` and `content icon normalize` take `--strict` (and the family key `icon_strict_family_ink: true`, off by default) to fail instead of warn when the dominant icon ink is not a family ink. Without a family palette the family-ink check is still not emitted. - `org labels ORG --repos-from-gh` plans labels across accessible repositories without the private estate registry, with paginated read-only GitHub capture, optional verified local checkouts, unchanged reference guards, and a documented org-default-forms-first workflow. Registry-backed plans still require the org registry row, and checkout repository names match GitHub case-insensitively, including resolved pending renames. Registry-free runs also isolate lazy brand lookups from estate decision files, and held reference rewrites retain their file/line guidance with an incomplete result. Workflow/labeler/release/Renovate references remain final skips rather than unresolved input requests. Missing Windows device-named checkouts fall back to captured metadata instead of aborting the org plan. Pending transfers check the repository's current owner's default issue forms and hold renames when that evidence is unavailable. Sparse or partially missing checkouts retain reference holds based on captured paths and the Git index, while unrelated omitted files remain allowed. +- `references/art-direction.md` documents the Codex image-generation recipe for the icon pipeline (the `codex exec` command, reading the image path from the run output, and the retry and attempt limits). Documentation only; no behaviour change. + ### Fixed - Windows contributor tests now use explicit UTF-8, native path basenames, a platform-independent read-only metadata stub, and Git Bash with stdin-safe Action scripts. All 15 issue #9 failures are individually documented; the full suite now runs in Windows, Linux and macOS CI. diff --git a/references/art-direction.md b/references/art-direction.md index 70e1959..19fbfcc 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -74,6 +74,37 @@ Starred values are approximate until the estate planner re-measures them and pub | Screenshot (B, E) | `assets/screenshot.webp` | 1600×1000 viewport | WebP q80 ≤ 300 KB; captured from the live URL in a browser, never drawn by an image model | | Provenance | `assets/PROMPTS.md`, `.github/profile/assets/PROMPTS.md` | — | Markdown, one block per committed generated asset | +## Codex image-generation recipe + +### Command + +The prompt comes from `content icon prompt` (written with `--out`, for example `art/prompts/ORG/REPO/icon.md`); give it to Codex non-interactively: + +```bash +codex exec --skip-git-repo-check --json "$(cat art/prompts/ORG/REPO/icon.md)" > run.jsonl +``` + +- `exec` runs one non-interactive turn. +- `--skip-git-repo-check` lets it run from a scratch directory. +- `--json` prints one JSON event per line, so the run output can be parsed (the image path is read from it, see below). + +Use the same model and version for a whole family run (pipeline rule 4) and record it in `PROMPTS.md`. Flag names follow the Codex CLI at the time of writing; if one is rejected, run `codex exec --help` and change nothing else in the recipe. + +### Read the image path from the run output + +1. After the run, search the `run.jsonl` events (or the final message) for the image file path Codex reports for the generated image, and use exactly that path. +2. Never pick the newest file in the generated-images folder. Parallel sessions write into the same place, so the newest file can belong to another run and the wrong image gets normalized. +3. If the run output names no image path, treat the attempt as failed. Do not fall back to the newest file or to a directory listing. +4. Pass the chosen path to `content icon normalize --in PATH`. In the `PROMPTS.md` provenance block, record the Codex session id and the path in the prose beside the record; the Provenance record fenced template itself does not change. +5. Use one run per candidate set. Do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. + +### Retry and attempt limits + +- A run that finishes with no image path is retried as a NEW run with the same prompt, at most 2 retries (3 attempts total) per candidate set. Parallel runs sometimes produce no path, and a retry has succeeded. +- Retries never alter the prompt. +- Attempts that produced a path count toward pipeline rule 7 (4 candidates, then 4 more once, then log `art-fail`). Empty attempts do not count as candidates but do count toward the 3-attempt cap. +- After the cap, stop and log `art-fail` with a one-line reason. Do not loop. + ## Repository glyph prompt template `content icon prompt` uses this self-contained option A template by default. Its diff --git a/references/content.md b/references/content.md index b2da981..fa5c4e0 100644 --- a/references/content.md +++ b/references/content.md @@ -59,4 +59,4 @@ both tokens and exits 4 with their names; add them to the custom config or pass tokens for every family. Option B uses the illustration palette and does not require these glyph tokens, whether art direction is selected implicitly or explicitly. -All actions run on a checkout and a metadata file alone. The registry and approvals refine the result when present (a renamed org's packages and links resolve to its current login; a registry `language:` override reaches the chip); without them the resolver uses `references/brand.yaml` and treats nothing as approved. `icon prompt`, `icon normalize` and `icon check` default to the skill's bundled [art-direction.md](art-direction.md), regardless of the working directory. Option A uses its repository glyph template and the family's `brand.yaml` tokens; option B uses its illustration template. Pass `--art-direction FILE` to select an explicit art direction file (relative paths resolve from the current directory), including an estate-format file with `(b) Family palette` and `(d) Shared prompt template` sections. A missing selected file reports `needs-input:art-direction` and names that path; `prompt` exits 4, while normalization and QA log the missing palette and continue their other checks. +All actions run on a checkout and a metadata file alone. The registry and approvals refine the result when present (a renamed org's packages and links resolve to its current login; a registry `language:` override reaches the chip); without them the resolver uses `references/brand.yaml` and treats nothing as approved. `icon prompt`, `icon normalize` and `icon check` default to the skill's bundled [art-direction.md](art-direction.md), regardless of the working directory. Option A uses its repository glyph template and the family's `brand.yaml` tokens; option B uses its illustration template. Pass `--art-direction FILE` to select an explicit art direction file (relative paths resolve from the current directory), including an estate-format file with `(b) Family palette` and `(d) Shared prompt template` sections. A missing selected file reports `needs-input:art-direction` and names that path; `prompt` exits 4, while normalization and QA log the missing palette and continue their other checks. The Codex recipe for producing the candidate image (command, how to read its path, retry limit) is in [art-direction.md](art-direction.md#codex-image-generation-recipe). diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py new file mode 100644 index 0000000..b9165fa --- /dev/null +++ b/tests/test_art_direction_recipe.py @@ -0,0 +1,63 @@ +"""E3.4.1: the Codex image-generation recipe in references/art-direction.md (doc contract, no behaviour).""" +import re + +from conftest import ROOT + +ART = ROOT / "references" / "art-direction.md" +HEAD = "## Codex image-generation recipe" +GLYPH = "## Repository glyph prompt template" + + +def _text(): + return ART.read_text(encoding="utf-8") + + +def _recipe(text): + start = text.find(HEAD) + if start < 0: + return "" + body = text[start + len(HEAD):] + m = re.search(r"^## ", body, re.M) + return HEAD + body[: m.start()] if m else HEAD + body + + +def test_recipe_section_exists_before_glyph_template(): + text = _text() + assert text.count(HEAD) == 1, "recipe heading must appear exactly once" + assert text.find(HEAD) < text.find(GLYPH) + assert _recipe(text), "recipe section not found" + + +def test_recipe_has_codex_exec_command(): + section = _recipe(_text()) + assert "codex exec" in section + assert "--json" in section + assert "--skip-git-repo-check" in section + fence = re.search(r"```bash\n(.*?)```", section, re.S) + assert fence, "command must sit in a ```bash fence" + assert "codex exec" in fence.group(1) + assert "--json" in fence.group(1) + assert "--skip-git-repo-check" in fence.group(1) + + +def test_recipe_reads_path_from_run_output_not_newest(): + lowered = _recipe(_text()).lower() + assert "never pick the newest" in lowered + assert "run output" in lowered + + +def test_recipe_retry_limit(): + section = _recipe(_text()) + assert "2 retries" in section + assert "3 attempts" in section + + +def test_recipe_has_no_local_paths(): + section = _recipe(_text()) + assert not re.search(r"[A-Za-z]:\\Users|/home/|/Users/", section) + + +def test_existing_sections_intact(): + text = _text() + for heading in ("## Prompt template for illustrations", GLYPH, "## Pipeline rules (MUST)"): + assert text.count(heading) == 1, heading From db902d4937d27772fa150ff34921115f00cdef60 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Fri, 30 Oct 2026 11:18:15 -0400 Subject: [PATCH 02/15] docs: address E3 review on the Codex recipe (E3.4.1, #65) - Remove the unverified "a retry has succeeded" outcome claim from the retry bullet; state only why a retry is allowed. - Add a test that the recipe section makes no verification claims. - Remove the blank line that split the new CHANGELOG bullet from the other Unreleased > Added bullets, so the list stays tight. Doc and test only; no behaviour change. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 - references/art-direction.md | 2 +- tests/test_art_direction_recipe.py | 8 ++++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 740f93e..3b1026c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon check` and `content icon normalize` take `--min-contrast` (and the family key `icon_min_contrast`, range 1 to 21) to set the contrast floor of the icon ink on the card. Precedence is the flag, then the family key, then the default 3.0, so existing results do not change. 4.5:1 is recommended for the card plate but is not the default. - `content icon check` and `content icon normalize` take `--strict` (and the family key `icon_strict_family_ink: true`, off by default) to fail instead of warn when the dominant icon ink is not a family ink. Without a family palette the family-ink check is still not emitted. - `org labels ORG --repos-from-gh` plans labels across accessible repositories without the private estate registry, with paginated read-only GitHub capture, optional verified local checkouts, unchanged reference guards, and a documented org-default-forms-first workflow. Registry-backed plans still require the org registry row, and checkout repository names match GitHub case-insensitively, including resolved pending renames. Registry-free runs also isolate lazy brand lookups from estate decision files, and held reference rewrites retain their file/line guidance with an incomplete result. Workflow/labeler/release/Renovate references remain final skips rather than unresolved input requests. Missing Windows device-named checkouts fall back to captured metadata instead of aborting the org plan. Pending transfers check the repository's current owner's default issue forms and hold renames when that evidence is unavailable. Sparse or partially missing checkouts retain reference holds based on captured paths and the Git index, while unrelated omitted files remain allowed. - - `references/art-direction.md` documents the Codex image-generation recipe for the icon pipeline (the `codex exec` command, reading the image path from the run output, and the retry and attempt limits). Documentation only; no behaviour change. ### Fixed diff --git a/references/art-direction.md b/references/art-direction.md index 19fbfcc..fbf5466 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -100,7 +100,7 @@ Use the same model and version for a whole family run (pipeline rule 4) and reco ### Retry and attempt limits -- A run that finishes with no image path is retried as a NEW run with the same prompt, at most 2 retries (3 attempts total) per candidate set. Parallel runs sometimes produce no path, and a retry has succeeded. +- A run that finishes with no image path is retried as a NEW run with the same prompt, at most 2 retries (3 attempts total) per candidate set. Parallel runs can finish without a path, which is why a retry is allowed. - Retries never alter the prompt. - Attempts that produced a path count toward pipeline rule 7 (4 candidates, then 4 more once, then log `art-fail`). Empty attempts do not count as candidates but do count toward the 3-attempt cap. - After the cap, stop and log `art-fail` with a one-line reason. Do not loop. diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index b9165fa..8edd068 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -52,6 +52,14 @@ def test_recipe_retry_limit(): assert "3 attempts" in section +def test_recipe_makes_no_verification_claims(): + # The recipe is documentation only: it must not report observed outcomes or + # verification (for example "a retry has succeeded") that nothing in the repo proves. + lowered = _recipe(_text()).lower() + for phrase in ("verified", "has succeeded", "tested on", "was tested", "confirmed on"): + assert phrase not in lowered, phrase + + def test_recipe_has_no_local_paths(): section = _recipe(_text()) assert not re.search(r"[A-Za-z]:\\Users|/home/|/Users/", section) From ca48d48bac58cdeed3f4501cca8f6f7a2bb8244d Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Fri, 30 Oct 2026 11:27:09 -0400 Subject: [PATCH 03/15] test: harden the Codex recipe guards (E3.4.1, #65) - Local-path guard now also rejects POSIX absolute (/tmp, /var), home-relative (~/), Windows drive-rooted (C:\ and C:/) and UNC paths. Regression cases cover each form. The reviewer's /tmp and C:\ examples passed the old regex; they are now caught. - Existing-content guard now pins the heading sequence, every table row and the provenance record template against the e54952a baseline, and checks the Family additions line. A renamed heading, a changed table row or an altered provenance template now fails. Test only; no behaviour change. Co-Authored-By: Claude Sonnet 5.5 --- tests/test_art_direction_recipe.py | 146 +++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index 8edd068..6b4e8b9 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -1,11 +1,100 @@ """E3.4.1: the Codex image-generation recipe in references/art-direction.md (doc contract, no behaviour).""" import re +import pytest + from conftest import ROOT ART = ROOT / "references" / "art-direction.md" HEAD = "## Codex image-generation recipe" GLYPH = "## Repository glyph prompt template" +PROMPT_TOKEN = "art/prompts/ORG/REPO/icon.md" + +# Baseline: references/art-direction.md at commit e54952a, before the recipe was added. +# Everything outside the recipe section must still match these values. Change them only +# together with a deliberate change to the settled art-direction rules (see AGENTS.md). +BASE_HEADINGS = """# Art direction +## Pipeline rules (MUST) +## Family palettes and motifs +## CompleteTech org motifs (banner and shared card illustration) +## Asset specification +## Repository glyph prompt template +## Prompt template for illustrations +## Provenance record (`PROMPTS.md`, one block per committed asset) +## assets/icon.png +## Art QA checklist (all MUST pass before commit; also the candidate rejection rules)""".splitlines() + +BASE_TABLE_ROWS = """| Family | Canvas | Primary | Accent | Text | Motif | Fonts | +|---|---|---|---|---|---|---| +| CompleteTech | ink `#0F172A` → navy `#001F3F`, 32 px `#1E3A8A` grid at 25% | sky `#00A7FD` → azure `#0081ED` → royal `#005ADD` | gold `#FFC300` (small details only); rose `#E11D48` only on the single finding object in Security art | `#F1F5F9`, `#CBD5E1`; muted `#94A3B8` | Isometric platform with satin-plastic objects linked by gold circuit traces | Poppins, JetBrains Mono | +| AI Ascension | ink `#15120E` / `#2A241C` | amber `#F2B75C` / `#E39B3A` | paper `#EDE4D3` | `#EDE4D3` | Existing system; banners kept | Fraunces, Bricolage Grotesque, JetBrains Mono | +| Jev Engineering | near-black `#010104` | cyan `#22D3EE`* | violet `#8B5CF6`* | `#F1F5F9` | A glowing central node fanning into lanes ending in a circle, square, triangle and diamond | Poppins, JetBrains Mono | +| jevplay.games | pink `#F386A1` with a halftone layer (12 px pitch, `#E551BA` at 20%) | magenta `#E551BA` | ink `#1E1E1E`, white | `#1E1E1E` | Retro pixel desktop windows and Jev, the boxy pixel robot | Existing wordmark; Press Start 2P (36 px or larger) | +| LibreEvolve | dark navy `#021527` | emerald `#10B981`* | faded emerald `#065F46`* | `#F1F5F9` | A lineage tree: most branches fade, one bright path continues | Poppins | +| Touched By An Agent | maroon `#1D1116` | amber `#F59E0B`* | magenta `#E11D74`* | `#FDF2F8` | One stylized robotic hand, signal ripples, a smart ring | Poppins | +| romgenie | dark teal `#002C31` | brass `#B8860B`* | lamp glow `#F4C430`* | `#F8FAFC` | Glossy 3D vault objects: chests, tapes, reels; a brass telescope and binoculars for the watching org | Poppins | +| Org | Motif objects | +|---|---| +| CompleteTech-LLC | cloud puff, blank document sheet, gear, chat bubble | +| CompleteTech-Skills | envelope with open flap, document sheet with blank ruled lines, coin stack, scroll with gold wax seal, certificate with gold ribbon rosette | +| CompleteTech-LLC-AI-Research | telescope aimed at a cluster of glowing node spheres | +| `CompleteTech-Security` | shield over rack server units, blank tag cards; one tag card in `#E11D48` as the finding | +| CompleteTech-Infrastructure | rack server unit, plug-in cube, cloud puff, circuit board | +| CompleteTech-Commerce | bar-chart panel, coin stack, briefcase, magnifier | +| CompleteTech-Creative | film reel, microphone, open book, chat bubble | +| CompleteTech-Pokemon | card stack, game controller, coin stack | +| CompleteTech-Data | database cylinder stack, map pin, bar-chart panel, folder | +| CompleteTech-Games | puzzle piece, game controller, card stack | +| CompleteTech-Directories | folder, map pin, magnifier, certificate with gold ribbon rosette | +| CompleteTech-Hardware | circuit board, gear, plug-in cube | +| CompleteTechDiscord-Games | chat bubble, game controller, puzzle piece | +| CompleteTech-DiscordApps | chat bubble, plug-in cube, bar-chart panel | +| CompleteTech-Patent-IP | blank document sheet, gold wax seal, magnifier | +| CompleteTech-Career-Toolkit | blank document sheet, briefcase, certificate with gold ribbon rosette | +| CompleteTech-ManageEngine | chat bubble, folder, cloud puff, gear | +| Asset | Path | Pixels | Format and limit | +|---|---|---|---| +| README hero banner | `docs/images/banner.png` | 1280×360 (or 2560×720) | PNG ≤ 300 KB, opaque; generated by `banner` | +| Repo card | `docs/images/card.svg` | 400×160 | SVG ≤ 40 KB; generated by `card` | +| Repository icon | `assets/icon.png` | 1024×1024 | PNG RGBA, transparent (drawn to read on the family card colour, `#F8FAFC` for CompleteTech) or with one opaque square ground (it then fills the card, full-bleed), ≤ 300 KB, no text | +| Brand files | `CompleteTech-LLC/.github/brand/`: `logo.png`, `logo-dark.png` (the exemplar pair, byte-identical), the chosen dark master (decision 19), `mark-dark.png` and `bulb-dark.png` once re-measured | as committed | PNG RGBA | +| Logo pair in Skills repositories | `assets/logo.png`, `assets/logo-dark.png` | byte-identical to the brand files | sha256 `a5c3c4ef55336ed4b883d3bc3bb6e1e942b836469bd2b2933bc8cc230b321621` (logo.png, 243,500 B) and `0dc00c0e5159518ff1d552b7052293b4e545ddd35be489e6d114e3ea64db27ec` (logo-dark.png, 210,785 B) | +| Social preview | `assets/social-preview.png` | 1280×640 | PNG ≤ 900 KB (heavier: JPG q85); hard limit under 1 MB; uploaded in the browser | +| Org avatar | `.github/profile/assets/avatar-500.png` (+ `avatar-1024.png` master) | 500×500 | Opaque PNG ≤ 300 KB; uploaded in the browser | +| Org banner | `.github/profile/assets/banner.jpg` | 1600×400 | JPG q85 ≤ 400 KB | +| Diagram source | `assets/diagrams/workflow.mmd` | — | Mermaid | +| Example preview | `assets/examples/example.png` | ≤ 2600 px wide | ≤ 600 KB | +| Screenshot (B, E) | `assets/screenshot.webp` | 1600×1000 viewport | WebP q80 ≤ 300 KB; captured from the live URL in a browser, never drawn by an image model | +| Provenance | `assets/PROMPTS.md`, `.github/profile/assets/PROMPTS.md` | — | Markdown, one block per committed generated asset | +| Family | Style lock (verbatim) | +|---|---| +| CompleteTech | Clean glossy isometric 3D product illustration, orthographic isometric camera from the front-left. Every scene sits on one rounded-rectangle platform: deep navy (#001F3F) top, ink (#0F172A) sides, a thin glowing sky-blue (#00A7FD) edge line, floating above an empty background with a soft contact shadow. Objects are smooth satin plastic in sky blue (#00A7FD), azure (#0081ED), royal blue (#005ADD) and off-white (#F1F5F9), with small warm-gold (#FFC300) details. Glass parts are frosted translucent blue. Thin gold circuit traces ending in round solder pads link the objects across the platform top. Soft rounded geometry, subtle bevels, bloom on gold details only, key light from the upper left, calm and uncluttered. | +| jevplay-games | Bold retro pixel-art illustration with chunky one-pixel dark outlines at a clearly visible pixel scale, flat colors on a plain flat pink background, and a playful early-desktop-computer aesthetic with simple window frames. | +| Jev-Engineering | Minimal glowing vector-style 3D render on a near-black background: smooth neon lines and nodes, soft cyan and violet glow, clean geometric primitives, calm technical mood, no clutter. | +| LibreEvolve | Minimal glowing illustration on dark navy: a luminous emerald branching structure, soft depth of field, most branches dim and fading, one path bright, calm scientific mood. | +| TouchedByAnAgent | Glossy 3D product illustration on deep maroon: smooth pink-peach robotics, warm amber glow, soft magenta signal ripples, tasteful and non-suggestive, studio lighting. | +| romgenie | Glossy illustrated 3D still life in a warm vault: brass-bound objects, soft lamp light from above, dark teal ambient background, storybook-premium finish. |""".splitlines() + +BASE_PROVENANCE = """## assets/icon.png +- Date: YYYY-MM-DD +- Tool and model: TOOL / MODEL ID AND VERSION +- Seed: SEED or "not supported" +- Reference images: golden reference path, jev-reference.png, or "none" +- Prompt: (verbatim, fenced) +- Candidates: 4 or 8; selected: N (reason: one line) +- Post-processing: showcase.py VERSION; output sha256 HASH; size KB +""" + +# A local path an operator could paste into the recipe: POSIX absolute or home-relative, +# Windows drive-rooted (backslash or slash) or UNC. Each must start a token (after +# whitespace, a backtick, a quote, a parenthesis or "="), so URLs and mid-word slashes +# such as art/prompts/ORG/REPO do not match. +LOCAL_PATH = re.compile( + r"(?:^|[\s`'\"(=])[A-Za-z]:[\\/]" # C:\x or C:/x + r"|(?:^|[\s`'\"(=])\\\\[A-Za-z0-9_.-]" # \\server\share + r"|(?:^|[\s`'\"(=])~?/[A-Za-z0-9_.-]", # /tmp/x, /home/x or ~/x + re.M, +) def _text(): @@ -21,6 +110,12 @@ def _recipe(text): return HEAD + body[: m.start()] if m else HEAD + body +def _outside_recipe(text): + recipe = _recipe(text) + assert recipe, "recipe section not found" + return text.replace(recipe, "", 1) + + def test_recipe_section_exists_before_glyph_template(): text = _text() assert text.count(HEAD) == 1, "recipe heading must appear exactly once" @@ -63,9 +158,60 @@ def test_recipe_makes_no_verification_claims(): def test_recipe_has_no_local_paths(): section = _recipe(_text()) assert not re.search(r"[A-Za-z]:\\Users|/home/|/Users/", section) + assert LOCAL_PATH.search(section) is None + + +def test_recipe_prompt_path_is_a_placeholder_token(): + # The guard below swaps this token for absolute paths; it must be present in the real section. + assert PROMPT_TOKEN in _recipe(_text()) + + +@pytest.mark.parametrize("bad", [ + "/tmp/private/icon.md", + "/var/folders/x/icon.md", + "/home/user/icon.md", + "/Users/user/icon.md", + "~/art/icon.md", + "C:\\private\\icon.md", + "C:/private/icon.md", + "C:\\Users\\user\\icon.md", + "\\\\server\\share\\icon.md", +]) +def test_local_path_guard_rejects_absolute_paths(bad): + section = _recipe(_text()).replace(PROMPT_TOKEN, bad) + assert PROMPT_TOKEN not in section + assert LOCAL_PATH.search(section), bad + + +def test_local_path_guard_ignores_urls_and_relative_paths(): + sample = "see https://www.complete.tech/a and art/prompts/ORG/REPO/icon.md and run.jsonl" + assert LOCAL_PATH.search(sample) is None def test_existing_sections_intact(): text = _text() for heading in ("## Prompt template for illustrations", GLYPH, "## Pipeline rules (MUST)"): assert text.count(heading) == 1, heading + + +def test_existing_heading_sequence_unchanged(): + headings = [line for line in _outside_recipe(_text()).splitlines() if re.match(r"#{1,6} ", line)] + assert headings == BASE_HEADINGS + + +def test_existing_tables_unchanged(): + rows = [line for line in _outside_recipe(_text()).splitlines() if line.startswith("|")] + assert rows == BASE_TABLE_ROWS + + +def test_existing_provenance_template_unchanged(): + text = _outside_recipe(_text()) + start = text.index("## Provenance record") + block = re.search(r"```markdown\n(.*?)```", text[start:], re.S) + assert block is not None, "provenance record fence missing" + assert block.group(1) == BASE_PROVENANCE + + +def test_family_additions_line_present_once(): + lines = [line for line in _text().splitlines() if line.startswith("- Family additions:")] + assert len(lines) == 1 From 5c2cd5733982428a7917d71039340443761d8389 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Fri, 30 Oct 2026 11:41:15 -0400 Subject: [PATCH 04/15] docs: add privacy and sandbox notes to the Codex recipe (E3.4.2, #66) Add Privacy and Sandbox subsections to the Codex image-generation recipe in references/art-direction.md: only public facts go into prompts, private repository content stays out, and images are read from the generated-images folder and never committed raw. Extend the recipe tests to cover them. Co-Authored-By: Claude Sonnet 5.5 --- references/art-direction.md | 15 +++++++++++++++ tests/test_art_direction_recipe.py | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/references/art-direction.md b/references/art-direction.md index fbf5466..c984826 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -105,6 +105,21 @@ Use the same model and version for a whole family run (pipeline rule 4) and reco - Attempts that produced a path count toward pipeline rule 7 (4 candidates, then 4 more once, then log `art-fail`). Empty attempts do not count as candidates but do count toward the 3-attempt cap. - After the cap, stop and log `art-fail` with a one-line reason. Do not loop. +### Privacy: only public facts go into prompts + +- (a) Prompts may contain only public facts: the public repository name, its public description and topics, the family palette and style lock, and the glyph fields supplied by the writer. +- (b) Never paste private-repository content into a prompt: no private repo names, README or source text, issue text, customer or personal data, emails, tokens, or estate-registry rows. +- (c) The prompt that `content icon prompt` writes is already limited to those public facts. Do not extend it by hand. Review it before running Codex, because the prompt leaves the machine. +- (d) For a private repository, do not generate the icon with Codex from its contents. Use only the family glyph fields and a neutral name, or hold the task. +- (e) Keep the prompt text in the provenance record verbatim, which makes this rule auditable. + +### Sandbox: read from the generated-images folder + +- (a) Codex stores generated images in its `generated_images` folder inside `$CODEX_HOME` (default: the `.codex` folder in the user's home, `~`). Read the image from the path named in the run output, which points there. +- (b) Files that Codex writes into its own sandbox, for example a copy it saves into the working directory it was given, can be locked against the user account (access denied). Do not rely on them, and do not try to chmod or take ownership of them. +- (c) If a path is unreadable, copy nothing from the sandbox. Re-take the image from the generated-images folder path in the run output, or retry the run under the retry limit above. +- (d) Copy the chosen image to a scratch location outside the repository before `content icon normalize --in`, and commit only the normalized `assets/icon.png`. Never commit the raw candidate. Do not write any real absolute local path or user name; use `$CODEX_HOME` and `~` only. + ## Repository glyph prompt template `content icon prompt` uses this self-contained option A template by default. Its diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index 6b4e8b9..fd22f12 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -215,3 +215,28 @@ def test_existing_provenance_template_unchanged(): def test_family_additions_line_present_once(): lines = [line for line in _text().splitlines() if line.startswith("- Family additions:")] assert len(lines) == 1 + + +def test_recipe_says_only_public_facts_in_prompts(): + lowered = _recipe(_text()).lower() + assert "only public facts" in lowered + + +def test_recipe_forbids_private_repository_content(): + lowered = _recipe(_text()).lower() + assert "private-repository content" in lowered + assert "registry" in lowered + + +def test_recipe_sandbox_note(): + lowered = _recipe(_text()).lower() + assert "generated_images" in lowered + assert "access denied" in lowered + assert "never commit the raw candidate" in lowered + + +def test_recipe_privacy_and_sandbox_headings(): + section = _recipe(_text()) + headings = [line for line in section.splitlines() if line.startswith("### ")] + assert any(h.startswith("### Privacy") for h in headings) + assert any(h.startswith("### Sandbox") for h in headings) From 6c0baf0ad2e87e843dde8a3da4a0eae9e4d1e375 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Fri, 30 Oct 2026 12:01:40 -0400 Subject: [PATCH 05/15] docs: state icon prompt generator does not filter private facts Co-Authored-By: Claude Sonnet 5.5 --- references/art-direction.md | 2 +- tests/test_art_direction_recipe.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/references/art-direction.md b/references/art-direction.md index c984826..b64f2b6 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -109,7 +109,7 @@ Use the same model and version for a whole family run (pipeline rule 4) and reco - (a) Prompts may contain only public facts: the public repository name, its public description and topics, the family palette and style lock, and the glyph fields supplied by the writer. - (b) Never paste private-repository content into a prompt: no private repo names, README or source text, issue text, customer or personal data, emails, tokens, or estate-registry rows. -- (c) The prompt that `content icon prompt` writes is already limited to those public facts. Do not extend it by hand. Review it before running Codex, because the prompt leaves the machine. +- (c) `content icon prompt` does not establish whether a fact is public. It writes repository metadata (name, description, topics) and fields derived from the local README, such as its heading, into the Markdown file that Codex receives, with no privacy filtering. Before running Codex, read the complete generated Markdown, including every README-derived field, and remove or replace anything that is not public. Use public-only inputs, or hold the task. The prompt leaves the machine. - (d) For a private repository, do not generate the icon with Codex from its contents. Use only the family glyph fields and a neutral name, or hold the task. - (e) Keep the prompt text in the provenance record verbatim, which makes this rule auditable. diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index fd22f12..b3749a8 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -222,6 +222,16 @@ def test_recipe_says_only_public_facts_in_prompts(): assert "only public facts" in lowered +def test_recipe_does_not_claim_generator_filters_privacy(): + # Reproduced on the base: `content icon prompt` copies repo metadata and the + # local README heading into the prompt with no public/private check. + lowered = _recipe(_text()).lower() + assert "already limited" not in lowered + assert "does not establish whether a fact is public" in lowered + assert "complete generated markdown" in lowered + assert "readme-derived" in lowered + + def test_recipe_forbids_private_repository_content(): lowered = _recipe(_text()).lower() assert "private-repository content" in lowered From f1880bc10f247ae131858e5d387496254c8920a7 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Fri, 30 Oct 2026 12:10:26 -0400 Subject: [PATCH 06/15] docs: record relative provenance path and apply Codex attempt cap per candidate Co-Authored-By: Claude Sonnet 5.5 --- references/art-direction.md | 10 +++++----- tests/test_art_direction_recipe.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/references/art-direction.md b/references/art-direction.md index b64f2b6..2a00de9 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -95,15 +95,15 @@ Use the same model and version for a whole family run (pipeline rule 4) and reco 1. After the run, search the `run.jsonl` events (or the final message) for the image file path Codex reports for the generated image, and use exactly that path. 2. Never pick the newest file in the generated-images folder. Parallel sessions write into the same place, so the newest file can belong to another run and the wrong image gets normalized. 3. If the run output names no image path, treat the attempt as failed. Do not fall back to the newest file or to a directory listing. -4. Pass the chosen path to `content icon normalize --in PATH`. In the `PROMPTS.md` provenance block, record the Codex session id and the path in the prose beside the record; the Provenance record fenced template itself does not change. -5. Use one run per candidate set. Do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. +4. Pass the chosen path to `content icon normalize --in PATH`. In the `PROMPTS.md` provenance block, record the Codex session id and only the image file name, or the path written relative to `$CODEX_HOME` (for example `$CODEX_HOME/generated_images/`), in the prose beside the record. Never record the absolute path: it contains the local user name. The Provenance record fenced template itself does not change. +5. Use one run per candidate. Do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. ### Retry and attempt limits -- A run that finishes with no image path is retried as a NEW run with the same prompt, at most 2 retries (3 attempts total) per candidate set. Parallel runs can finish without a path, which is why a retry is allowed. +- The limit applies per candidate, not per candidate set. A run that finishes with no image path is retried as a NEW run with the same prompt, at most 2 retries (3 attempts total) for that candidate. Parallel runs can finish without a path, which is why a retry is allowed. - Retries never alter the prompt. -- Attempts that produced a path count toward pipeline rule 7 (4 candidates, then 4 more once, then log `art-fail`). Empty attempts do not count as candidates but do count toward the 3-attempt cap. -- After the cap, stop and log `art-fail` with a one-line reason. Do not loop. +- A run that returns a path produces one candidate and ends that candidate's attempts. Successful candidates never count toward the cap, so the four-candidate round of pipeline rule 7 (then 4 more once, then log `art-fail`) is still reachable. Only empty runs count toward the 3 attempts, and they are not candidates. +- After the cap for any one candidate, stop and log `art-fail` with a one-line reason. Do not loop. ### Privacy: only public facts go into prompts diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index b3749a8..29f4aae 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -250,3 +250,26 @@ def test_recipe_privacy_and_sandbox_headings(): headings = [line for line in section.splitlines() if line.startswith("### ")] assert any(h.startswith("### Privacy") for h in headings) assert any(h.startswith("### Sandbox") for h in headings) + + +def test_recipe_provenance_never_records_absolute_path(): + # The generated-images folder is inside $CODEX_HOME (the user's home), so the + # absolute path holds the local user name. Provenance records the file name or a + # $CODEX_HOME-relative path only. + section = _recipe(_text()) + step = next(l for l in section.splitlines() if l.startswith("4. ")) + assert "$CODEX_HOME" in step + assert "file name" in step + assert "never record the absolute path" in step.lower() + assert "record the codex session id and the path" not in step.lower() + + +def test_recipe_attempt_cap_is_per_candidate(): + # Successful runs are candidates and must not exhaust the cap, otherwise the + # four-candidate round (pipeline rule 7) could never complete. + section = _recipe(_text()) + lowered = section.lower() + assert "per candidate, not per candidate set" in lowered + assert "per candidate set" not in lowered.replace("per candidate, not per candidate set", "") + assert "successful candidates never count toward the cap" in lowered + assert "only empty runs count toward the 3 attempts" in lowered From ea77a79d7148f8431fae0b84916bae843ae1573d Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Fri, 30 Oct 2026 12:14:15 -0400 Subject: [PATCH 07/15] Fix E741 ambiguous variable name in art-direction recipe test Co-Authored-By: Claude Sonnet 5.5 --- tests/test_art_direction_recipe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index 29f4aae..dc8dc19 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -257,7 +257,7 @@ def test_recipe_provenance_never_records_absolute_path(): # absolute path holds the local user name. Provenance records the file name or a # $CODEX_HOME-relative path only. section = _recipe(_text()) - step = next(l for l in section.splitlines() if l.startswith("4. ")) + step = next(ln for ln in section.splitlines() if ln.startswith("4. ")) assert "$CODEX_HOME" in step assert "file name" in step assert "never record the absolute path" in step.lower() From 1e0bdeec758739c32459b4f87b3a27e16c390147 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:18:35 -0400 Subject: [PATCH 08/15] test: pin recipe base table rows to merged main art-direction brand rows Co-Authored-By: Claude Sonnet 5.5 --- tests/test_art_direction_recipe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index dc8dc19..6f39999 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -57,8 +57,8 @@ | README hero banner | `docs/images/banner.png` | 1280×360 (or 2560×720) | PNG ≤ 300 KB, opaque; generated by `banner` | | Repo card | `docs/images/card.svg` | 400×160 | SVG ≤ 40 KB; generated by `card` | | Repository icon | `assets/icon.png` | 1024×1024 | PNG RGBA, transparent (drawn to read on the family card colour, `#F8FAFC` for CompleteTech) or with one opaque square ground (it then fills the card, full-bleed), ≤ 300 KB, no text | -| Brand files | `CompleteTech-LLC/.github/brand/`: `logo.png`, `logo-dark.png` (the exemplar pair, byte-identical), the chosen dark master (decision 19), `mark-dark.png` and `bulb-dark.png` once re-measured | as committed | PNG RGBA | -| Logo pair in Skills repositories | `assets/logo.png`, `assets/logo-dark.png` | byte-identical to the brand files | sha256 `a5c3c4ef55336ed4b883d3bc3bb6e1e942b836469bd2b2933bc8cc230b321621` (logo.png, 243,500 B) and `0dc00c0e5159518ff1d552b7052293b4e545ddd35be489e6d114e3ea64db27ec` (logo-dark.png, 210,785 B) | +| Brand files | `assets/brand/` in this repository (the published masters the generators and the footer use): `logo.png`, `logo-dark.png` (the exemplar pair), `logo-dark-full.png` (the candidate dark master, decision 19) and `mark-light.png`. `mark-dark.png` and `bulb-dark.png` are not shipped yet (see `assets/brand/README.md`). The former `CompleteTech-LLC/.github/brand/` location is historical only: those URLs returned 404, and the footer serves the files from `assets/brand/` (same sha256). | as committed | PNG; see `assets/brand/README.md` for modes | +| Logo pair in Skills repositories | `assets/logo.png`, `assets/logo-dark.png` | byte-identical to the `assets/brand/` masters (same sha256) | sha256 `a5c3c4ef55336ed4b883d3bc3bb6e1e942b836469bd2b2933bc8cc230b321621` (logo.png, 243,500 B) and `0dc00c0e5159518ff1d552b7052293b4e545ddd35be489e6d114e3ea64db27ec` (logo-dark.png, 210,785 B) | | Social preview | `assets/social-preview.png` | 1280×640 | PNG ≤ 900 KB (heavier: JPG q85); hard limit under 1 MB; uploaded in the browser | | Org avatar | `.github/profile/assets/avatar-500.png` (+ `avatar-1024.png` master) | 500×500 | Opaque PNG ≤ 300 KB; uploaded in the browser | | Org banner | `.github/profile/assets/banner.jpg` | 1600×400 | JPG q85 ≤ 400 KB | From 1597e69aa1d1c70aa63a27b8d8aa1da4f462599e Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:38:00 -0400 Subject: [PATCH 09/15] test: add static wording guards for #64 recipe observed-runtime and candidate contract Static wording tests (mock evidence, not an image-generation run) that assert the observed-runtime labelling, exact returned-path rule, no-unverified-URL rule, and per-slot candidate contract in references/art-direction.md. Existing tests are unchanged. Co-Authored-By: Claude Haiku 5.5 --- tests/test_art_direction_recipe.py | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index 6f39999..db505cb 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -273,3 +273,45 @@ def test_recipe_attempt_cap_is_per_candidate(): assert "per candidate set" not in lowered.replace("per candidate, not per candidate set", "") assert "successful candidates never count toward the cap" in lowered assert "only empty runs count toward the 3 attempts" in lowered + + +def test_recipe_labels_sandbox_facts_as_observed_and_uses_exact_returned_path(): + # Static wording test (mock evidence, not an image-generation run). + lowered = _recipe(_text()).lower() + assert "observed runtime notes" in lowered + assert "issue #64" in lowered + assert "not behaviour documented by openai" in lowered + assert "exact path returned by the run output" in lowered + assert "does not rely on a documented image path" in lowered + assert "codex-cli 0.166.0" in lowered + assert "at the time of writing" in lowered + for flag in ("--skip-git-repo-check", "--json", "--sandbox", "--output-last-message"): + assert flag in lowered, flag + assert "never invent a model id" in lowered + assert "not exposed" in lowered + + +def test_recipe_does_not_cite_unverified_url_for_image_path(): + # Static wording test (mock evidence, not an image-generation run). + assert "http" not in _recipe(_text()).lower() + + +def test_recipe_candidate_contract_is_consistent_with_four_candidate_template(): + # Static wording test: the fixed generator template line stays byte-identical and unique, + # and the recipe handles every returned path instead of one run per candidate. + text = _text() + assert text.count("Produce four candidates. {SEED_INSTRUCTION}") == 1 + assert text.count("Produce four candidates.") == 1 + section = _recipe(text) + lowered = section.lower() + assert "one run per candidate" not in lowered + assert "every distinct path the run output reports is one candidate" in lowered + assert "between zero and four image paths" in lowered + assert "empty run" in lowered + assert "four candidate slots" in lowered + assert "the cap applies to each slot" in lowered + assert "same unchanged prompt" in lowered + assert "new run" in lowered + assert "2 retries" in lowered and "3 attempts" in lowered + assert "after 3 empty attempts for any one slot" in lowered + assert "art-fail" in lowered From d0b7a9921d3ffc8bfa3c3e4712f4732012f7d154 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:38:12 -0400 Subject: [PATCH 10/15] docs: align Codex recipe with candidate contract and observed notes - Add codex exec flags (--sandbox, --output-last-message, stdin '-') and state codex-cli 0.166.0 at the time of writing. - Record model id only as reported, or 'not exposed'; never invent one. - Label generated_images and sandbox-lock facts as observed runtime notes, not OpenAI documentation; always use the exact path from the run output. - Each distinct returned path is a candidate; empty runs are retried per candidate slot (max 3 attempts), same unchanged prompt. Co-Authored-By: Claude Haiku 5.5 --- references/art-direction.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/references/art-direction.md b/references/art-direction.md index 0ea332d..d9e0e28 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -87,8 +87,13 @@ codex exec --skip-git-repo-check --json "$(cat art/prompts/ORG/REPO/icon.md)" > - `exec` runs one non-interactive turn. - `--skip-git-repo-check` lets it run from a scratch directory. - `--json` prints one JSON event per line, so the run output can be parsed (the image path is read from it, see below). +- `--sandbox MODE` selects the sandbox policy for model-generated shell commands. +- `--output-last-message FILE` also writes the final message to FILE. +- A prompt of `-` is read from standard input. -Use the same model and version for a whole family run (pipeline rule 4) and record it in `PROMPTS.md`. Flag names follow the Codex CLI at the time of writing; if one is rejected, run `codex exec --help` and change nothing else in the recipe. +Use the same model and version for a whole family run (pipeline rule 4). Record the tool, model id and version in `PROMPTS.md` exactly as the run reports them; if the run does not expose a model id, write `not exposed` in the `TOOL / MODEL ID AND VERSION` field. Never invent a model id. Flag names follow codex-cli 0.166.0 at the time of writing; if one is rejected, run `codex exec --help` and change nothing else in the recipe. + +This recipe does not rely on a documented image path: OpenAI documentation covers non-interactive `codex exec` and its JSON events, not where generated images are stored, so always use the exact path returned by the run output. ### Read the image path from the run output @@ -96,14 +101,14 @@ Use the same model and version for a whole family run (pipeline rule 4) and reco 2. Never pick the newest file in the generated-images folder. Parallel sessions write into the same place, so the newest file can belong to another run and the wrong image gets normalized. 3. If the run output names no image path, treat the attempt as failed. Do not fall back to the newest file or to a directory listing. 4. Pass the chosen path to `content icon normalize --in PATH`. In the `PROMPTS.md` provenance block, record the Codex session id and only the image file name, or the path written relative to `$CODEX_HOME` (for example `$CODEX_HOME/generated_images/`), in the prose beside the record. Never record the absolute path: it contains the local user name. The Provenance record fenced template itself does not change. -5. Use one run per candidate. Do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. +5. One invocation may report between zero and four image paths. Every DISTINCT path the run output reports is one candidate; use exactly the reported paths, never a directory listing or the newest file. A run that reports no path is an empty run. To tell outputs apart, do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. ### Retry and attempt limits -- The limit applies per candidate, not per candidate set. A run that finishes with no image path is retried as a NEW run with the same prompt, at most 2 retries (3 attempts total) for that candidate. Parallel runs can finish without a path, which is why a retry is allowed. -- Retries never alter the prompt. -- A run that returns a path produces one candidate and ends that candidate's attempts. Successful candidates never count toward the cap, so the four-candidate round of pipeline rule 7 (then 4 more once, then log `art-fail`) is still reachable. Only empty runs count toward the 3 attempts, and they are not candidates. -- After the cap for any one candidate, stop and log `art-fail` with a one-line reason. Do not loop. +- The limit applies per candidate, not per candidate set. The fixed prompt asks for four candidates, so there are four candidate slots, and the cap applies to each slot. A run that returns k distinct paths fills k slots (up to the unfilled ones); make further runs, with the same unchanged prompt, only for unfilled slots. +- A run that finishes with no image path is an empty run. Retry it as a NEW run with the same prompt, at most 2 retries (3 attempts total) for that slot. Parallel runs can finish without a path, which is why a retry is allowed. Retries never alter the prompt. +- Successful candidates never count toward the cap, so the four-candidate round of pipeline rule 7 (then 4 more once, then log `art-fail`) is still reachable. Only empty runs count toward the 3 attempts, and they are not candidates. +- After 3 empty attempts for any one slot, stop and log `art-fail` with a one-line reason. Do not loop. ### Privacy: only public facts go into prompts @@ -115,8 +120,10 @@ Use the same model and version for a whole family run (pipeline rule 4) and reco ### Sandbox: read from the generated-images folder -- (a) Codex stores generated images in its `generated_images` folder inside `$CODEX_HOME` (default: the `.codex` folder in the user's home, `~`). Read the image from the path named in the run output, which points there. -- (b) Files that Codex writes into its own sandbox, for example a copy it saves into the working directory it was given, can be locked against the user account (access denied). Do not rely on them, and do not try to chmod or take ownership of them. +These are OBSERVED runtime notes from issue #64 and the Pick Your AI evidence, not behaviour documented by OpenAI. Do not cite OpenAI documentation for them; the rule that holds regardless is: use the exact path returned by the run output. + +- (a) Observed: Codex stored generated images in its `generated_images` folder inside `$CODEX_HOME` (default: the `.codex` folder in the user's home, `~`). Read the image from the path named in the run output, which points there. +- (b) Observed: files that Codex writes into its own sandbox, for example a copy it saves into the working directory it was given, can be locked against the user account (access denied). Do not rely on them, and do not try to chmod or take ownership of them. - (c) If a path is unreadable, copy nothing from the sandbox. Re-take the image from the generated-images folder path in the run output, or retry the run under the retry limit above. - (d) Copy the chosen image to a scratch location outside the repository before `content icon normalize --in`, and commit only the normalized `assets/icon.png`. Never commit the raw candidate. Do not write any real absolute local path or user name; use `$CODEX_HOME` and `~` only. From 2bb3e3ae9e54169248bcd8e54a52dd359c98eee2 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:38:48 -0400 Subject: [PATCH 11/15] docs: state observed-runtime labelling and per-slot candidate contract in #64 recipe Docs and static wording tests only; no behaviour change. Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Claude Haiku 5.5 --- CHANGELOG.md | 2 +- references/art-direction.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b262e..8d857cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon check` and `content icon normalize` take `--min-contrast` (and the family key `icon_min_contrast`, range 1 to 21) to set the contrast floor of the icon ink on the card. Precedence is the flag, then the family key, then the default 3.0, so existing results do not change. 4.5:1 is recommended for the card plate but is not the default. - `content icon check` and `content icon normalize` take `--strict` (and the family key `icon_strict_family_ink: true`, off by default) to fail instead of warn when the dominant icon ink is not a family ink. Without a family palette the family-ink check is still not emitted. - `org labels ORG --repos-from-gh` plans labels across accessible repositories without the private estate registry, with paginated read-only GitHub capture, optional verified local checkouts, unchanged reference guards, and a documented org-default-forms-first workflow. Registry-backed plans still require the org registry row, and checkout repository names match GitHub case-insensitively, including resolved pending renames. Registry-free runs also isolate lazy brand lookups from estate decision files, and held reference rewrites retain their file/line guidance with an incomplete result. Workflow/labeler/release/Renovate references remain final skips rather than unresolved input requests. Missing Windows device-named checkouts fall back to captured metadata instead of aborting the org plan. Pending transfers check the repository's current owner's default issue forms and hold renames when that evidence is unavailable. Sparse or partially missing checkouts retain reference holds based on captured paths and the Git index, while unrelated omitted files remain allowed. -- `references/art-direction.md` documents the Codex image-generation recipe for the icon pipeline (the `codex exec` command, reading the image path from the run output, and the retry and attempt limits). Documentation only; no behaviour change. +- `references/art-direction.md` documents the Codex image-generation recipe for the icon pipeline (the `codex exec` command, reading the image path from the run output, and the retry and attempt limits). Image-folder and sandbox notes are labelled as observed runtime behaviour, not OpenAI-documented, and the exact returned path is the rule. Each distinct returned path is one candidate, and the attempt cap is per candidate slot (four slots, 3 empty attempts each). Documentation and static wording tests only; no behaviour change. ### Fixed diff --git a/references/art-direction.md b/references/art-direction.md index d9e0e28..c017db1 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -101,7 +101,7 @@ This recipe does not rely on a documented image path: OpenAI documentation cover 2. Never pick the newest file in the generated-images folder. Parallel sessions write into the same place, so the newest file can belong to another run and the wrong image gets normalized. 3. If the run output names no image path, treat the attempt as failed. Do not fall back to the newest file or to a directory listing. 4. Pass the chosen path to `content icon normalize --in PATH`. In the `PROMPTS.md` provenance block, record the Codex session id and only the image file name, or the path written relative to `$CODEX_HOME` (for example `$CODEX_HOME/generated_images/`), in the prose beside the record. Never record the absolute path: it contains the local user name. The Provenance record fenced template itself does not change. -5. One invocation may report between zero and four image paths. Every DISTINCT path the run output reports is one candidate; use exactly the reported paths, never a directory listing or the newest file. A run that reports no path is an empty run. To tell outputs apart, do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. +5. One invocation may report between zero and four image paths. Every DISTINCT path the run output reports is one candidate; use exactly the reported paths, never a directory listing or the newest file. A run that reports no path is an empty run. Parallel sessions write into the same place, so do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. ### Retry and attempt limits From 2ff0a4986e6c0f30ff912463f3e35ec1b5760ea2 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:53:14 -0400 Subject: [PATCH 12/15] test: guard unreadable-only candidate runs against the slot budget (#64) Add a static wording test asserting that a run reporting only unreadable paths is an empty run that fills no slot and counts against each unfilled slot's 3-attempt budget, ending in art-fail. Also asserts the sandbox item (c) wording no longer allows unbounded retries. Co-Authored-By: Claude Haiku 5.5 --- tests/test_art_direction_recipe.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index db505cb..5180446 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -315,3 +315,28 @@ def test_recipe_candidate_contract_is_consistent_with_four_candidate_template(): assert "2 retries" in lowered and "3 attempts" in lowered assert "after 3 empty attempts for any one slot" in lowered assert "art-fail" in lowered + + +def test_recipe_unreadable_only_output_counts_against_slot_budget(): + # Static wording test (mock evidence, not an image-generation run): a run whose only + # reported paths are unreadable must fill no slot and count as an empty run, so the + # per-slot budget of 3 attempts still terminates with art-fail. + section = _recipe(_text()) + lowered = section.lower() + assert "a run that reports no readable path is an empty run" in lowered + assert "unreadable-only" in lowered + assert "an unreadable path fills no slot" in lowered + assert "counts as one attempt against every slot that is still unfilled" in lowered + assert "unreadable-only output is counted as an empty run" in lowered + assert "after 3 empty attempts for any one slot, including attempts that returned only unreadable paths" in lowered + assert "art-fail" in lowered + sandbox_c = [ + line.strip().lower() + for line in section.splitlines() + if line.strip().lower().startswith("- (c) if a path is unreadable") + ] + assert len(sandbox_c) == 1, "sandbox item (c) must appear exactly once" + assert "empty run" in sandbox_c[0] + assert "one attempt" in sandbox_c[0] + assert "art-fail" in sandbox_c[0] + assert "retry the run under the retry limit" not in sandbox_c[0] From f17925dff7244d6cd031e3ea257b3a1c9f576b98 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:53:39 -0400 Subject: [PATCH 13/15] docs: treat unreadable image paths as empty runs in the Codex recipe Unreadable-only Codex output now counts as an empty run against every unfilled slot, so the recipe has a bounded three-attempt stop and cannot loop. Reworded the path, retry, unreadable-path and sandbox rules to match. Prompt template line unchanged. Co-Authored-By: Claude Haiku 5.5 --- references/art-direction.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/references/art-direction.md b/references/art-direction.md index c017db1..676a840 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -99,16 +99,16 @@ This recipe does not rely on a documented image path: OpenAI documentation cover 1. After the run, search the `run.jsonl` events (or the final message) for the image file path Codex reports for the generated image, and use exactly that path. 2. Never pick the newest file in the generated-images folder. Parallel sessions write into the same place, so the newest file can belong to another run and the wrong image gets normalized. -3. If the run output names no image path, treat the attempt as failed. Do not fall back to the newest file or to a directory listing. +3. If the run output names no readable image path, treat the attempt as an empty run (see Retry and attempt limits). Do not fall back to the newest file or to a directory listing. 4. Pass the chosen path to `content icon normalize --in PATH`. In the `PROMPTS.md` provenance block, record the Codex session id and only the image file name, or the path written relative to `$CODEX_HOME` (for example `$CODEX_HOME/generated_images/`), in the prose beside the record. Never record the absolute path: it contains the local user name. The Provenance record fenced template itself does not change. -5. One invocation may report between zero and four image paths. Every DISTINCT path the run output reports is one candidate; use exactly the reported paths, never a directory listing or the newest file. A run that reports no path is an empty run. Parallel sessions write into the same place, so do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. +5. One invocation may report between zero and four image paths. Every DISTINCT path the run output reports is one candidate; use exactly the reported paths, never a directory listing or the newest file. A run that reports no readable path is an empty run: either it reports no path, or every path it reports is unreadable (unreadable-only output). An unreadable path fills no slot, and readable paths in the same run still fill their slots. Parallel sessions write into the same place, so do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. ### Retry and attempt limits -- The limit applies per candidate, not per candidate set. The fixed prompt asks for four candidates, so there are four candidate slots, and the cap applies to each slot. A run that returns k distinct paths fills k slots (up to the unfilled ones); make further runs, with the same unchanged prompt, only for unfilled slots. -- A run that finishes with no image path is an empty run. Retry it as a NEW run with the same prompt, at most 2 retries (3 attempts total) for that slot. Parallel runs can finish without a path, which is why a retry is allowed. Retries never alter the prompt. -- Successful candidates never count toward the cap, so the four-candidate round of pipeline rule 7 (then 4 more once, then log `art-fail`) is still reachable. Only empty runs count toward the 3 attempts, and they are not candidates. -- After 3 empty attempts for any one slot, stop and log `art-fail` with a one-line reason. Do not loop. +- The limit applies per candidate, not per candidate set. The fixed prompt asks for four candidates, so there are four candidate slots, and the cap applies to each slot. A run that returns k distinct readable paths fills k slots (up to the unfilled ones); make further runs, with the same unchanged prompt, only for unfilled slots. +- An empty run (no path, or unreadable-only output) is retried as a NEW run with the same unchanged prompt, at most 2 retries (3 attempts total) for each slot that is still unfilled. Each empty run counts as one attempt against every slot that is still unfilled. Parallel runs can finish without a path, which is why a retry is allowed. Retries never alter the prompt. +- Successful candidates never count toward the cap, so the four-candidate round of pipeline rule 7 (then 4 more once, then log `art-fail`) is still reachable. Only empty runs count toward the 3 attempts, and they are not candidates. An unreadable path is not a successful candidate, so unreadable-only output is counted as an empty run and cannot loop forever. +- After 3 empty attempts for any one slot, including attempts that returned only unreadable paths, stop and log `art-fail` with a one-line reason. Do not loop. ### Privacy: only public facts go into prompts @@ -124,7 +124,7 @@ These are OBSERVED runtime notes from issue #64 and the Pick Your AI evidence, n - (a) Observed: Codex stored generated images in its `generated_images` folder inside `$CODEX_HOME` (default: the `.codex` folder in the user's home, `~`). Read the image from the path named in the run output, which points there. - (b) Observed: files that Codex writes into its own sandbox, for example a copy it saves into the working directory it was given, can be locked against the user account (access denied). Do not rely on them, and do not try to chmod or take ownership of them. -- (c) If a path is unreadable, copy nothing from the sandbox. Re-take the image from the generated-images folder path in the run output, or retry the run under the retry limit above. +- (c) If a path is unreadable, copy nothing from the sandbox and do not retry the same path. Treat that path as returning nothing: if the same run reports another readable path in the generated-images folder, use that; otherwise the run is an empty run and counts as one attempt under the retry limit above, ending in `art-fail` after 3 empty attempts for the slot. - (d) Copy the chosen image to a scratch location outside the repository before `content icon normalize --in`, and commit only the normalized `assets/icon.png`. Never commit the raw candidate. Do not write any real absolute local path or user name; use `$CODEX_HOME` and `~` only. ## Repository glyph prompt template From e4cdb394e51965cb62bd1b6775ea066105304dc9 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:54:40 -0400 Subject: [PATCH 14/15] docs: note unreadable-only output counts as an empty attempt in #64 changelog Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d857cd..3335ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon check` and `content icon normalize` take `--min-contrast` (and the family key `icon_min_contrast`, range 1 to 21) to set the contrast floor of the icon ink on the card. Precedence is the flag, then the family key, then the default 3.0, so existing results do not change. 4.5:1 is recommended for the card plate but is not the default. - `content icon check` and `content icon normalize` take `--strict` (and the family key `icon_strict_family_ink: true`, off by default) to fail instead of warn when the dominant icon ink is not a family ink. Without a family palette the family-ink check is still not emitted. - `org labels ORG --repos-from-gh` plans labels across accessible repositories without the private estate registry, with paginated read-only GitHub capture, optional verified local checkouts, unchanged reference guards, and a documented org-default-forms-first workflow. Registry-backed plans still require the org registry row, and checkout repository names match GitHub case-insensitively, including resolved pending renames. Registry-free runs also isolate lazy brand lookups from estate decision files, and held reference rewrites retain their file/line guidance with an incomplete result. Workflow/labeler/release/Renovate references remain final skips rather than unresolved input requests. Missing Windows device-named checkouts fall back to captured metadata instead of aborting the org plan. Pending transfers check the repository's current owner's default issue forms and hold renames when that evidence is unavailable. Sparse or partially missing checkouts retain reference holds based on captured paths and the Git index, while unrelated omitted files remain allowed. -- `references/art-direction.md` documents the Codex image-generation recipe for the icon pipeline (the `codex exec` command, reading the image path from the run output, and the retry and attempt limits). Image-folder and sandbox notes are labelled as observed runtime behaviour, not OpenAI-documented, and the exact returned path is the rule. Each distinct returned path is one candidate, and the attempt cap is per candidate slot (four slots, 3 empty attempts each). Documentation and static wording tests only; no behaviour change. +- `references/art-direction.md` documents the Codex image-generation recipe for the icon pipeline (the `codex exec` command, reading the image path from the run output, and the retry and attempt limits). Image-folder and sandbox notes are labelled as observed runtime behaviour, not OpenAI-documented, and the exact returned path is the rule. Each distinct returned path is one candidate, and the attempt cap is per candidate slot (four slots, 3 empty attempts each); unreadable-only output counts as an empty attempt and ends in `art-fail` after 3. Documentation and static wording tests only; no behaviour change. ### Fixed From 8a61efff5b947f2d149aa8111f0cf9cfc31cc577 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 00:59:49 -0400 Subject: [PATCH 15/15] docs: apply four accepted Opus P3 fixes to Codex image recipe (#64) Item 1 reads every reported image path, the retry bullet distinguishes pipeline rule 7 from the option A template, the second rule 7 round opens four new slots with the same per-slot cap, and item 4 normalizes a scratch copy. Adds one static test asserting all four (local evidence only). Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Claude Haiku 5.5 --- references/art-direction.md | 7 ++++--- tests/test_art_direction_recipe.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/references/art-direction.md b/references/art-direction.md index 676a840..380837b 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -97,17 +97,18 @@ This recipe does not rely on a documented image path: OpenAI documentation cover ### Read the image path from the run output -1. After the run, search the `run.jsonl` events (or the final message) for the image file path Codex reports for the generated image, and use exactly that path. +1. After the run, search the `run.jsonl` events (or the final message) for every image file path Codex reports for the generated images, and use exactly those paths (see item 5). 2. Never pick the newest file in the generated-images folder. Parallel sessions write into the same place, so the newest file can belong to another run and the wrong image gets normalized. 3. If the run output names no readable image path, treat the attempt as an empty run (see Retry and attempt limits). Do not fall back to the newest file or to a directory listing. -4. Pass the chosen path to `content icon normalize --in PATH`. In the `PROMPTS.md` provenance block, record the Codex session id and only the image file name, or the path written relative to `$CODEX_HOME` (for example `$CODEX_HOME/generated_images/`), in the prose beside the record. Never record the absolute path: it contains the local user name. The Provenance record fenced template itself does not change. +4. Copy the chosen image to scratch (see Sandbox (d)) and pass that copy to content icon normalize --in PATH, not the generated_images path. In the `PROMPTS.md` provenance block, record the Codex session id and only the image file name, or the path written relative to `$CODEX_HOME` (for example `$CODEX_HOME/generated_images/`), in the prose beside the record. Never record the absolute path: it contains the local user name. The Provenance record fenced template itself does not change. 5. One invocation may report between zero and four image paths. Every DISTINCT path the run output reports is one candidate; use exactly the reported paths, never a directory listing or the newest file. A run that reports no readable path is an empty run: either it reports no path, or every path it reports is unreadable (unreadable-only output). An unreadable path fills no slot, and readable paths in the same run still fill their slots. Parallel sessions write into the same place, so do not run candidates for several repositories in the same working directory at once unless you can tell their outputs apart. ### Retry and attempt limits -- The limit applies per candidate, not per candidate set. The fixed prompt asks for four candidates, so there are four candidate slots, and the cap applies to each slot. A run that returns k distinct readable paths fills k slots (up to the unfilled ones); make further runs, with the same unchanged prompt, only for unfilled slots. +- The limit applies per candidate, not per candidate set. Pipeline rule 7 asks for four candidates (the option A template also says so), so there are four candidate slots, and the cap applies to each slot. A run that returns k distinct readable paths fills k slots (up to the unfilled ones); make further runs, with the same unchanged prompt, only for unfilled slots. - An empty run (no path, or unreadable-only output) is retried as a NEW run with the same unchanged prompt, at most 2 retries (3 attempts total) for each slot that is still unfilled. Each empty run counts as one attempt against every slot that is still unfilled. Parallel runs can finish without a path, which is why a retry is allowed. Retries never alter the prompt. - Successful candidates never count toward the cap, so the four-candidate round of pipeline rule 7 (then 4 more once, then log `art-fail`) is still reachable. Only empty runs count toward the 3 attempts, and they are not candidates. An unreadable path is not a successful candidate, so unreadable-only output is counted as an empty run and cannot loop forever. +- The second round of rule 7 ("4 more once") opens four new slots with the same per-slot cap of 3 attempts (2 retries each); slots from the first round do not carry over their attempt counts. - After 3 empty attempts for any one slot, including attempts that returned only unreadable paths, stop and log `art-fail` with a one-line reason. Do not loop. ### Privacy: only public facts go into prompts diff --git a/tests/test_art_direction_recipe.py b/tests/test_art_direction_recipe.py index 5180446..e7cfbe2 100644 --- a/tests/test_art_direction_recipe.py +++ b/tests/test_art_direction_recipe.py @@ -340,3 +340,25 @@ def test_recipe_unreadable_only_output_counts_against_slot_budget(): assert "one attempt" in sandbox_c[0] assert "art-fail" in sandbox_c[0] assert "retry the run under the retry limit" not in sandbox_c[0] + + +def test_recipe_opus_p3_fixes_plural_paths_rule7_slots_and_scratch_copy(): + # Static wording test (mock evidence, not an image-generation run): the four accepted P3 findings. + text = _text() + assert text.count("Produce four candidates.") == 1 + lowered = _recipe(text).lower() + # (1) item 1 reads every image path the run reports, not a single path + assert "every image file path codex reports" in lowered + assert "use exactly those paths (see item 5)" in lowered + assert "use exactly that path" not in lowered + # (2) the first retry bullet distinguishes pipeline rule 7 and the option A template + assert "pipeline rule 7 asks for four candidates" in lowered + assert "the option a template also says so" in lowered + assert "the fixed prompt asks for four candidates" not in lowered + assert "four candidate slots" in lowered + # (3) the second round of rule 7 opens four new slots with the same per-slot cap + assert "the second round of rule 7" in lowered + assert "four new slots with the same per-slot cap of 3 attempts" in lowered + # (4) item 4 normalizes a scratch copy of the chosen image, not the generated path + assert "copy the chosen image to scratch (see sandbox (d)) and pass that copy to content icon normalize --in path" in lowered + assert "pass the chosen path to `content icon normalize" not in lowered