Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ck",
"description": "Code Katz personas and workflows for Claude Code: the product-definition pipeline (/ck:opportunity, market-research, brief, prd, team, roadmap, architecture, brand-guide, design), /ck:panel (three lenses on three models), /ck:plan and /ck:start (a staged plan, then one named session per stage), /ck:do (one small task, quick and in the chat), /ck:next, /ck:claude-md (a project CLAUDE.md from ck's template), /ck:cost (what a session cost, by lane and kind of token, also posted to its pull request at merge), the memory gate (a proposal and your approval before any memory is saved), a team per project (/ck:team invite and drop, kept in .claude/ck-team.conf), and the full persona roster, a complete cross-domain team, as ck:<name> subagents and /ck:<name> switch commands.",
"version": "0.1.28",
"version": "0.1.29",
"author": { "name": "Code Katz" }
}
18 changes: 18 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ Auto-maintained via [claude-devlog-skill](https://github.com/code-katz/claude-de

---

## [2026-11-07] 0.1.29: every ck page names its project

**Category:** `fix`
**Tags:** `ck`, `review-page`, `gallery`, `do`
**Risk Level:** `low`
**Breaking Change:** `no` (callers pass the same flags)

### Summary
A d20mob staged plan was published as "Phase 9, the ten: staged plan" under the banner line "Code Katz · review page", so nothing on the page said d20mob, and Will could not tell it apart in his artifact list. The review page skill asked for `--title "<product>: <document>"`, but nothing enforced it, and /ck:plan passed a title with no product. Now the renderers find the project's name themselves.

### Detail
- **The review page.** `scripts/render-review.py` puts the project's name in the banner line ("<project> · review page") and at the front of the `<title>` and the heading ("<project>: <title>"), and skips the prefix when the title already starts with the name. The name is the main checkout's folder of the git repository that holds `--in` (`git rev-parse --path-format=absolute --git-common-dir`, then its parent), so a page built in a worktree still names the project; outside a repository it is the file's folder. `--project` overrides it. Every gate (/ck:prd, /ck:plan, /ck:claude-md and the rest) gets it with no change.
- **The gallery pages.** A `git grep` across `skills/`, `workflows/` and `scripts/` found two other ways a page gets published. `scripts/render-gallery.py` (brand galleries, design galleries, and `chosen.html`) had "Code Katz · gallery review" and "Code Katz · chosen design"; it now names the project the same way, from the repository that holds the variants JSON, with the same `--project` flag. The gallery contract says so. The workflows that call it need no change.
- **The /ck:do mockup.** A persona writes `mockup.html` by hand and the session publishes it. The step's rules now ask for the `<title>` `<project>: <what you drew>` and one line above the mockup, `<project> · mockup`, with the session filling in the name.
- `scripts/project_name.py` holds the lookup both renderers use. One test renders a page from a file in a temporary repository named `demo`, then with `--project x`.

---

## [2026-11-06] 0.1.28: two endings, never "nothing is needed"

**Category:** `fix`
Expand Down
26 changes: 26 additions & 0 deletions scripts/project_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""project_name.py: the name of the project a file belongs to, for the pages ck publishes.

The name is the folder of the git repository's main checkout, found from the common git
directory, so a file in a worktree under .claude/worktrees/ still names the project. With
no repository, it is the file's own folder. Imported by render-review.py and render-gallery.py.
"""
import pathlib, re, subprocess

def project_name(path):
d = pathlib.Path(path).resolve()
if not d.is_dir(): d = d.parent
while not d.exists(): d = d.parent
try:
out = subprocess.run(['git', '-C', str(d), 'rev-parse', '--path-format=absolute', '--git-common-dir'],
capture_output=True, text=True, check=True).stdout.strip()
except (OSError, subprocess.CalledProcessError):
return d.name
common = pathlib.Path(out)
return common.parent.name if common.name == '.git' else re.sub(r'\.git$', '', common.name)

def _key(s): return re.sub(r'[^a-z0-9]', '', s.lower())

def with_project(title, project):
"""'<project>: <title>', unless the title already starts with the project's name."""
if not project or _key(title).startswith(_key(project)): return title
return f'{project}: {title}' if title else project
11 changes: 9 additions & 2 deletions scripts/render-gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@
<dir>/<label>/<screen>.<state>.html its empty, loading, and error states (and success, with --chosen)
A brand variant without a mark or a surface, or a design screen without its file, is an error that names the file.

The project's name leads the banner line and the title, found as render-review.py finds it, from the
git repository that holds the JSON; --project overrides it.

Standard library only. Every asset is inline; the page loads nothing but Google Fonts. Fails if the output
would exceed 16 MB or if labels are not consecutive capital letters from A.
"""
import argparse, html, json, pathlib, re, sys
from project_name import project_name, with_project

STEPS = """<ol>
<li>Open this link signed in to your Claude account.</li>
Expand Down Expand Up @@ -164,10 +168,12 @@ def main():
ap = argparse.ArgumentParser()
ap.add_argument('--in', dest='src'); ap.add_argument('--dir'); ap.add_argument('--out', required=True)
ap.add_argument('--chosen', help='render only this label as the chosen design, with its success states, no strip, no comment steps')
ap.add_argument('--project', help="the project's name; default: the main checkout's folder of the git repository holding the JSON")
a = ap.parse_args()
if not a.src and not a.dir: sys.exit('give --in <variants.json> or --dir <round directory>')
src = pathlib.Path(a.src) if a.src else pathlib.Path(a.dir) / 'variants.json'
if not src.exists(): sys.exit(f'{src} does not exist')
project = a.project or project_name(src)
d = json.loads(src.read_text())
kind = d.get('kind', 'brand'); product = d.get('product', ''); rnd = d.get('round', '')
variants = d.get('variants') or []
Expand All @@ -185,6 +191,7 @@ def main():
if a.dir: fill_from_dir(d, pathlib.Path(a.dir), states)
title = f'{product}: {rnd}' if rnd else product
if chosen: title = f'{product}: {rnd.split(":")[0]}, chosen variant {chosen}' if rnd else f'{product}: chosen variant {chosen}'
title = with_project(title, project)
strip = ''.join(f'<a href="#variant-{esc(l)}">{esc(l)} · {esc(v.get("name",""))}</a>' for l, v in zip(labels, variants))
skin = f' {esc(d["skin"])}' if d.get('skin') else ''
body = []
Expand All @@ -195,10 +202,10 @@ def main():
f'{render}<h3>Rationale</h3><p>{esc(v.get("rationale",""))}</p><h3>Trade-off</h3><p>{esc(v.get("tradeoff",""))}</p>'
f'<h3>Satisfies</h3><p>{esc(v.get("satisfies",""))}</p>{design_states(v, states) if kind == "design" else ""}</section>')
if chosen:
head = (f'<header class="banner"><div class="banner-inner"><div><p class="eyebrow">Code Katz · chosen design</p><h1>{esc(title)}</h1>'
head = (f'<header class="banner"><div class="banner-inner"><div><p class="eyebrow">{esc(project)} · chosen design</p><h1>{esc(title)}</h1>'
f'<p class="meta">{esc(variants[0].get("name",""))}: every screen with its default, empty, loading, error, and success states.{skin}</p></div></div></header>\n')
else:
head = (f'<header class="banner"><div class="banner-inner"><div><p class="eyebrow">Code Katz · gallery review</p><h1>{esc(title)}</h1>'
head = (f'<header class="banner"><div class="banner-inner"><div><p class="eyebrow">{esc(project)} · gallery review</p><h1>{esc(title)}</h1>'
f'<p class="meta">{len(variants)} labeled variants. Comment on the one you want, and on what to change.{skin}</p></div>'
f'<div class="howto"><h2>How to comment</h2>{STEPS}</div></div></header>\n'
f'<nav class="strip" aria-label="Variants">{strip}</nav>\n')
Expand Down
16 changes: 12 additions & 4 deletions scripts/render-review.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@

Usage:
render-review.py --in docs/PRD.md --out .ck/runs/<id>/review.html --title "Ardennes Hour PRD" \
[--question "Imagine this shipped and did not move the number. What went wrong?"] [--meta "Revision 1"]
[--question "Imagine this shipped and did not move the number. What went wrong?"] [--meta "Revision 1"] [--project name]

The project's name leads the banner line and the title ("<project>: <title>", unless the title
already starts with it). It is the main checkout's folder of the git repository that holds --in,
or --in's folder outside a repository; --project overrides it.

Standard library only. The page carries the five comment steps in its banner, a sticky
table of contents from the ## and ### headings, tables and code that scroll inside their
own container, and light and dark themes. Publish the output with the Artifact tool.
"""
import argparse, html, re, sys, pathlib
from project_name import project_name, with_project

def slug(text):
s = re.sub(r'[^a-z0-9]+', '-', text.lower()).strip('-')
Expand Down Expand Up @@ -206,7 +211,10 @@ def main():
ap.add_argument('--title', required=True)
ap.add_argument('--question', default='')
ap.add_argument('--meta', default='')
ap.add_argument('--project', help="the project's name; default: the main checkout's folder of the git repository holding --in")
a = ap.parse_args()
project = a.project or project_name(a.src)
title = with_project(a.title, project)
md = pathlib.Path(a.src).read_text()
body, toc = render(md)
q = ''
Expand All @@ -217,9 +225,9 @@ def main():
mermaid = ('<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.1/mermaid.min.js"></script>\n'
'<script>mermaid.initialize({startOnLoad:true,theme:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"neutral"});</script>\n'
) if 'class="mermaid"' in body else ''
page = (f'<title>{html.escape(a.title)}</title>\n<style>{CSS}</style>\n{mermaid}'
f'<header class="banner"><div class="banner-inner"><div><p class="eyebrow">Code Katz · review page</p>'
f'<h1>{html.escape(a.title)}</h1>{meta}</div>'
page = (f'<title>{html.escape(title)}</title>\n<style>{CSS}</style>\n{mermaid}'
f'<header class="banner"><div class="banner-inner"><div><p class="eyebrow">{html.escape(project)} · review page</p>'
f'<h1>{html.escape(title)}</h1>{meta}</div>'
f'<div class="howto"><div><h2>How to comment</h2>{STEPS}</div>{q}</div></div></header>\n'
f'<div class="page"><nav class="toc" aria-label="Contents">{toc_html(toc)}</nav><article>\n{body}\n</article></div>\n')
pathlib.Path(a.out).parent.mkdir(parents=True, exist_ok=True)
Expand Down
2 changes: 1 addition & 1 deletion skills/do/draw.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# /ck:do: a draw step

The step's rules: "Draw one mockup, not variants, unless the author asked for options (then at most three on one page, labeled A, B, C). Write only one file, `<runDir>/mockup.html`, and change nothing else: self-contained HTML with styles inline, no script, and nothing loaded from elsewhere except Google Fonts. When the project has mockups of its own, match their look. Use the product's real names, words, and colors, and show the change in place with enough of the screen around it to judge it. Return the file's path and three lines: what you drew, what you left out, and anything the author should decide."
The step's rules: "Draw one mockup, not variants, unless the author asked for options (then at most three on one page, labeled A, B, C). Write only one file, `<runDir>/mockup.html`, and change nothing else: self-contained HTML with styles inline, no script, and nothing loaded from elsewhere except Google Fonts. When the project has mockups of its own, match their look. Use the product's real names, words, and colors, and show the change in place with enough of the screen around it to judge it. The page's `<title>` is `<project>: <what you drew>`, and one thin line above the mockup reads `<project> · mockup`. Return the file's path and three lines: what you drew, what you left out, and anything the author should decide." Before you send it, replace `<project>` with the project's name, the folder that `basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"` prints, so the page can be told apart in the artifact list.

When it is back, read the file once (the Artifact tool requires it, and it is material), then publish it with the Artifact tool. It is a private link, not a review page; publish every later version from the same file path so the link stays the same. Say "Mockup is ready: `<link>`.", then the persona's left-out and to-decide lines, one line each, then "OK, or what should change?" and stop. When the Artifact tool is not available or is refused, do not retry in this run: give the file's full path in place of the link and say it opens in a browser.

Expand Down
4 changes: 2 additions & 2 deletions skills/gallery-artifact/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ One page shape for brand galleries and design galleries, so a review is the same

## The page, in order

1. **Banner**: the product name; what is being reviewed and the round or revision; the four comment steps, verbatim:
1. **Banner**: the project's name, in the banner line and at the front of the page's title (`scripts/render-gallery.py` finds it from the git repository); the product name; what is being reviewed and the round or revision; the four comment steps, verbatim:
1. Open this link signed in to your Claude account.
2. Switch the page to comment mode from the bar at the top.
3. Click the variant or the passage you want to comment on and type. Do not send the comment to Claude; your session reads it.
Expand Down Expand Up @@ -38,5 +38,5 @@ One page shape for brand galleries and design galleries, so a review is the same
2. Every variant has every part listed above, in order (States only for design galleries).
3. No external asset references other than Google Fonts.
4. The file is under 16 MB.
5. The banner carries the four steps verbatim.
5. The banner carries the four steps verbatim, and the banner line and the `<title>` name the project.
6. For a design gallery: every PRD requirement named under Satisfies exists in `docs/PRD.md`.
4 changes: 2 additions & 2 deletions skills/review-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Every document or gallery that needs a decision is reviewed on a page in Claude'

## 1. Publish

1. Build the page with one command, never by writing HTML yourself: `python3 "${CLAUDE_PLUGIN_ROOT}/scripts/render-review.py" --in <the document> --out <runDir>/review.html --title "<product>: <document>" --question "<the gate's question, if any>" --meta "<revision and date>"`. It renders the markdown with the banner below, a sticky table of contents so a comment can point at a section, tables and code that scroll inside their own container, and light and dark themes. For a gallery, the workflow has already written the page to the gallery contract; publish that file.
2. The banner carries, in this order: the product name and what is being reviewed; the round or revision; the one question the gate asks, when it has one (the PRD's premortem question, for example); and these four steps, verbatim (the renderer writes them; a gallery page must carry them too):
1. Build the page with one command, never by writing HTML yourself: `python3 "${CLAUDE_PLUGIN_ROOT}/scripts/render-review.py" --in <the document> --out <runDir>/review.html --title "<product>: <document>" --question "<the gate's question, if any>" --meta "<revision and date>"`. It puts the project's name (the main checkout's folder of the repository that holds the document) at the front of the banner line and the title, so the page can be told apart in the artifact list; `--project <name>` overrides it. It renders the markdown with the banner below, a sticky table of contents so a comment can point at a section, tables and code that scroll inside their own container, and light and dark themes. For a gallery, the workflow has already written the page to the gallery contract; publish that file.
2. The banner carries, in this order: the project's name and what is being reviewed; the round or revision; the one question the gate asks, when it has one (the PRD's premortem question, for example); and these four steps, verbatim (the renderer writes them; a gallery page must carry them too):
1. Open this link signed in to your Claude account.
2. Switch the page to comment mode from the bar at the top.
3. Click the passage or the variant you want to comment on and type. Do not send the comment to Claude; this session reads it.
Expand Down
9 changes: 9 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,15 @@ python3 scripts/render-review.py --in "$TMP/bar-code.md" --out "$TMP/bar-code.ht
printf '# T\n\n| Rule | Note |\n|---|---|\n| a \\| b | one |\n' > "$TMP/bar-esc.md"
python3 scripts/render-review.py --in "$TMP/bar-esc.md" --out "$TMP/bar-esc.html" --title "Bar" >/dev/null 2>&1 \
&& grep -qF '<tr><td>a | b</td><td>one</td></tr>' "$TMP/bar-esc.html" && ok "render-review.py keeps an escaped bar in its table cell" || fail "render-review.py splits a table cell at an escaped bar"
mkdir -p "$TMP/demo" && git -C "$TMP/demo" init -q && printf '# T\n\nBody.\n' > "$TMP/demo/doc.md"
if python3 scripts/render-review.py --in "$TMP/demo/doc.md" --out "$TMP/demo.html" --title "Plan" >/dev/null 2>&1 \
&& grep -qF '<title>demo: Plan</title>' "$TMP/demo.html" && grep -qF '>demo · review page<' "$TMP/demo.html" \
&& python3 scripts/render-review.py --in "$TMP/demo/doc.md" --out "$TMP/x.html" --title "Plan" --project x >/dev/null 2>&1 \
&& grep -qF '<title>x: Plan</title>' "$TMP/x.html" && grep -qF '>x · review page<' "$TMP/x.html"; then
ok "render-review.py names the project from its git repository, or from --project"
else
fail "render-review.py does not name the project in the title and banner"
fi
if grep -q 'libs/mermaid' "$TMP/bold.html"; then fail "render-review.py loads mermaid on a page without a diagram"; else ok "render-review.py loads mermaid only when a page has a diagram"; fi

# ─── 13. Gallery renderer ────────────────────────────────────────────────────
Expand Down