diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 06a278a..dd03626 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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: subagents and /ck: switch commands.", - "version": "0.1.28", + "version": "0.1.29", "author": { "name": "Code Katz" } } diff --git a/DEVLOG.md b/DEVLOG.md index ac37370..28c6071 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -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 ": "`, 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 (" · review page") and at the front of the `` 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` diff --git a/scripts/project_name.py b/scripts/project_name.py new file mode 100644 index 0000000..66c4b97 --- /dev/null +++ b/scripts/project_name.py @@ -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 diff --git a/scripts/render-gallery.py b/scripts/render-gallery.py index 55adae7..4c89879 100755 --- a/scripts/render-gallery.py +++ b/scripts/render-gallery.py @@ -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> @@ -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 [] @@ -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 = [] @@ -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') diff --git a/scripts/render-review.py b/scripts/render-review.py index 7014951..a10e33d 100755 --- a/scripts/render-review.py +++ b/scripts/render-review.py @@ -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('-') @@ -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 = '' @@ -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)}\n\n{mermaid}' - f'