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.26",
"version": "0.1.27",
"author": { "name": "Code Katz" }
}
25 changes: 25 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ Auto-maintained via [claude-devlog-skill](https://github.com/code-katz/claude-de

---

## [2026-11-05] 0.1.27: the model and effort follow the job

**Category:** `feature`
**Tags:** `ck`, `plan`, `start`, `tiers`, `effort`, `cost`, `claude-md`
**Risk Level:** `low`
**Breaking Change:** `no` (a Who cell in the older form is still read, by its model's tier)

### Summary
F1 of the releases page after 0.1.24, approved by Will as D1 on 2026-11-05. A stage ran on its owner's tier, so Akira worked on Fable whether the job was a design call or a rename, every stage inherited its coordinator's effort, and Will ruled on d20mob rows by hand. 45% of his spend ran on Fable and 62% at `xhigh`. The lanes test found one persona on the model that fits the job wrote better code in five of six blind verdicts, and the effort evidence (Anthropic's SWE-bench Pro runs, cost per solved task) set the point each kind of job runs at.

### Detail
- **Job words.** Who reads `<Persona>, <job>`, one of `design` (Opus 5.5 at high), `build`, `review` and `write` (Opus at medium) and `edit` (Sonnet 5.5 at medium). Each is a `job` line in `tiers.conf`, so moving a job to another point is one line. `<Persona>, <job>, <Model> <effort>` pins any other point on the four-model, five-effort grid. The persona lines stay for `/ck:do`, the workflows and `/ck:panel`.
- **Older plans.** `<Persona>, <Model>` is read by its tier: Fable as `design`, Opus as `build`, Sonnet or Haiku as `edit`. `plan.py check` adds one note line saying how many rows give that form. No plan file is rewritten.
- **The card.** `plan.py card` names the session `[P2-S4] [Akira design] <title>`, prints `job`, the model's id and `effort`, and the model check now covers both. A build card carries Will's goal line, "As much coded as necessary, as little code as possible." `rows` and `ready` print the point, such as `Opus high`, where they printed the model word.
- **Opening and checking a stage.** `/ck:start` passes `model` and `effort` on the chip. A chip cannot open above its coordinator on either axis; when the point is above, the coordinator says so and the stage's own check stops it. A session reads its own effort with `get_session` and `self`, checked in this build. The board's refresh sets a running stage's effort as well as its model.
- **The Fable rule.** The plan contract and the draft workflow pin `Fable high` only for a stage that Opus at high has already fallen short on, or, on trial, for security or concurrency code, and never `xhigh` or `max` unless the author asks.
- **How we work 0.1.27.** The session title's tag is `[{persona} {job}]`, and "The stage's model" becomes "The stage's model and effort", with the five points in words for a brief that names none.
- **Cost by point.** `cost.py` keeps each reply's effort, logs the cost by model and effort with each pull request, and `/ck:cost history` adds a table of cost per point for pull requests merged from 0.1.27 on.

### Open
- d20mob and hireling take How we work 0.1.27 with `/ck:claude-md`, on their own pull requests, when Will says so. Until then their own rule reads the tag's second word as a model; the card's check covers ck's cards.
- Left for later: re-running a failed build stage at high, moving the six Fable personas in `tiers.conf`, and job words in `/ck:do`.

---

## [2026-11-05] 0.1.26: the drawing's link in How we work

**Category:** `fix`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Phase one, first release in progress. What runs today:
| `/ck:next` | Shows the whole pipeline as a table (each step in order, what it does, the document it writes, and where your project stands), then says what to run next in one sentence | Built |
| `/ck:brief <idea>` | Toni runs a short market pass, River writes `docs/brief.md` (from your sentence, or from `docs/opportunity.md`), a checker validates it; you review on a page you can comment on | Built; drilled 2026-10-04 on a real project |
| `/ck:plan [work]` | Quinn lays the work out as a staged plan: phases, then stages, one job and one owner per row, each with its state. With nothing after it, the whole release from the roadmap (`docs/build-plan.md`); with a piece of work after it, that one item (`docs/plan/`). You review it on a page you can comment on | Built; not yet run on a real project |
| `/ck:start [stage]` | Opens the plan's sessions and keeps its board. For a stage it offers a card that starts a session for that one job, named from the row, such as `[A31-P2-S1] [Akira Fable] Make the creature tables`; with nothing after it, it updates the State column from the sessions and their pull requests and offers every row that could start now. With `fix <stage> <what is wrong>` it offers a card for a session that fixes one fault in a stage that is done, named such as `[A31-P1-S5 fix] [Akira Fable] Mend the scan's natural 1`; the stage stays done. Every new session checks its own model before it works. You click each card | Built; not yet run on a real project |
| `/ck:start [stage]` | Opens the plan's sessions and keeps its board. For a stage it offers a card that starts a session for that one job, named from the row, such as `[A31-P2-S1] [Akira design] Make the creature tables`; with nothing after it, it updates the State column from the sessions and their pull requests and offers every row that could start now. With `fix <stage> <what is wrong>` it offers a card for a session that fixes one fault in a stage that is done, named such as `[A31-P1-S5 fix] [Akira design] Mend the scan's natural 1`; the stage stays done. The row's job word (design, build, edit, review, write) sets each session's model and effort, from `tiers.conf`, and every new session checks both before it works. You click each card | Built; not yet run on a real project |
| `/ck:do <task>` | The quick path, all in the chat: a plan of a few lines (who does what), you say go, the right personas work in the background on their own tiers, a mockup comes back as a link when you ask for one first, and a build waits on its own branch until you say merge. No documents and no review pages | Built; tried in d20mob on 2026-10-27 |
| `/ck:claude-md` | Writes or updates the "How we work" section of this project's `CLAUDE.md` from ck's template (`templates/how-we-work/`), tailored to the project: its lanes, main folder, records and secrets filled in, a rule that does not fit changed or left out. It sorts the project's memory into keep, retire, or move. You review the section on a page you can comment on; it is written on a branch with a draft pull request only when you approve. The rest of `CLAUDE.md` is left as it is, and nothing runs unless you type the command | Built; not yet run on a real project |
| `/ck:cost [history \| <session id>]` | What this session has cost so far: a table with a row per lane (the main session, each persona, Claude Code's own background calls) and a column per kind of token (input, output, cache write, cache read), each cell in tokens and dollars, priced from Anthropic's pricing page and checked against Claude Code's own figure. Once the session has a pull request, a line after each answer shows the cost so far. At merge, the session gets one line with the pull request's cost, and the pull request gets a comment with the table and the numbers as hidden data. A pull request's cost is what the sessions that worked on its branch spent since their previous merge; a session that only merged it is shown apart. `history` lists each pull request's cost | Built; tried on d20mob sessions |
Expand Down
39 changes: 27 additions & 12 deletions scripts/cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ def add(row, t, dollars):


def read(transcript, prices):
"""{(lane, model): {kind: [tokens, dollars or None]}}, the title, Claude Code's record, the pull request link."""
"""{(lane, model): {kind: [tokens, dollars or None]}}, the title, Claude Code's record, the pull request link, and
{"<model> <effort>": [tokens, dollars or None]} over the transcripts' replies, by the effort each one ran at."""
rows = defaultdict(lambda: {k: [0, 0.0] for k in KINDS})
points = defaultdict(lambda: {k: [0, 0.0] for k in KINDS})
names, state, pr, seen = {}, None, None, set()
base = Path(transcript)
files = [(base, "main")]
Expand All @@ -138,19 +140,21 @@ def read(transcript, prices):
continue
kind = d.get("type")
if kind == "assistant" and isinstance(d.get("message"), dict):
last[d["message"].get("id") or id(d)] = d["message"]
last[d["message"].get("id") or id(d)] = d["message"], d.get("effort")
elif lane == "main" and kind in ("custom-title", "ai-title"):
names[kind] = d.get("customTitle") or d.get("aiTitle") or names.get(kind)
elif lane == "main" and kind == "cost-state":
state = d
elif lane == "main" and kind == "pr-link":
pr = d
for key, m in last.items():
for key, (m, effort) in last.items():
if key in seen or not isinstance(m.get("usage"), dict) or m.get("model") == "<synthetic>":
continue
seen.add(key)
t = tokens(m["usage"])
add(rows[(lane, m.get("model"))], t, priced(prices, m.get("model"), t))
dollars = priced(prices, m.get("model"), t)
add(rows[(lane, m.get("model"))], t, dollars)
add(points[" ".join(filter(None, (short(m.get("model")), effort)))], t, dollars)
for model, v in ((state or {}).get("modelUsage") or {}).items():
mine = combine(r for (_, mo), r in list(rows.items()) if mo == model)
extra = {k: max(0, (v.get(f) or 0) - mine[k][0]) for k, f in zip(KINDS, FIELDS)}
Expand All @@ -165,7 +169,7 @@ def read(transcript, prices):
d["write"] = min(max(rest, d["write"]), hi)
add(rows[("background", model)], t, d)
title = names.get("custom-title") or names.get("ai-title") or "Session " + base.stem[:8]
return rows, title, state, pr
return rows, title, state, pr, {p: list(total(r)) for p, r in points.items()}


def combine(rows):
Expand Down Expand Up @@ -207,7 +211,7 @@ def short(model):


def table(session, prices, source):
rows, title, state, _ = session
rows, title, state = session[:3]
out = [f"**{title} cost summary**", "", "| Lane | Model " + HEAD, "|---|---|--:|--:|--:|--:|--:|"]
for lane, model in sorted(rows, key=lambda r: (r[0] != "main", r[0] == "background", r[0], r[1] or "")):
r = rows[(lane, model)]
Expand Down Expand Up @@ -239,7 +243,7 @@ def newest(cwd):


def stop(hook, prices, source):
rows, _, _, pr = read(hook["transcript_path"], prices)
rows, _, _, pr, _ = read(hook["transcript_path"], prices)
if not pr:
return
models = {m: total(combine(r for (_, mo), r in rows.items() if mo == m)) for _, m in rows}
Expand Down Expand Up @@ -347,7 +351,7 @@ def version():

def charge(path, prices, log):
"""One session's totals now, and what it spent since its previous row in the log."""
rows, title, state, pr = read(path, prices)
rows, title, state, pr, points = read(path, prices)
whole = combine(rows.values())
n, d = total(whole)
prev = next((e for e in reversed(log) if e.get("session") == Path(path).stem), None)
Expand All @@ -356,7 +360,8 @@ def charge(path, prices, log):
return {"session": Path(path).stem, "title": title, "tokens": n, "dollars": d,
"cost_tokens": n - (prev.get("tokens") or 0) if prev else n,
"cost": minus(d, prev.get("dollars")) if prev else d,
"by_kind": {k: whole[k] for k in KINDS}, "by_model": models, "opening": opening, "requests": requests,
"by_kind": {k: whole[k] for k in KINDS}, "by_model": models, "by_point": points,
"opening": opening, "requests": requests,
"table": (rows, title, state, pr)}


Expand Down Expand Up @@ -387,7 +392,8 @@ def gh(*args):
body += ["", table(b["table"], prices, source)]
if b["cost"] != b["dollars"]:
body.append(f"The table is the whole session; {money(b['cost'])} of it came after its previous merge.")
keep = ("session", "title", "cost", "cost_tokens", "dollars", "tokens", "by_kind", "by_model", "opening", "requests")
keep = ("session", "title", "cost", "cost_tokens", "dollars", "tokens", "by_kind", "by_model", "by_point", "opening",
"requests")
data = {"pr": name, "url": info["url"], "commit": commit, "merged_at": info.get("mergedAt"), "cost": cost,
"tokens": sum(b["cost_tokens"] for b in built), "ck": version(),
"builders": [{k: b[k] for k in keep} for b in built], "merged_by": merged and {k: merged[k] for k in keep}}
Expand All @@ -400,7 +406,8 @@ def gh(*args):
for role, s in [("built", b) for b in built] + ([("merged", merged)] if merged else []):
f.write(json.dumps({"date": time.strftime("%Y-%m-%d"), "url": info["url"], "commit": commit, "session": s["session"],
"title": s["title"], "tokens": s["tokens"], "dollars": s["dollars"], "role": role,
"ck": data["ck"], "opening": s["opening"], "requests": s["requests"]}) + "\n")
"ck": data["ck"], "opening": s["opening"], "requests": s["requests"],
"by_point": s["by_point"]}) + "\n")

if merged:
line = (f"ck cost for {name}: {money(cost)}, built by {', '.join(b['title'] for b in built)}. "
Expand All @@ -415,12 +422,17 @@ def gh(*args):
def history():
"""One row per pull request: its cost, and the merging session's when another session built it. A row in the log
holds a session's running total, so each cost is that row minus the session's row before it."""
log, last, prs = entries(), {}, {}
log, last, prs, points = entries(), {}, {}, defaultdict(float)
for e in log:
if not e.get("url") or not e.get("session"):
continue
prev = last.get(e["session"])
spent = minus(e.get("dollars"), prev.get("dollars")) if prev else e.get("dollars")
if e.get("role", "built") == "built" and "by_point" in e:
# A row holds running totals, so a point's cost is this row's figure less the session's previous one.
before = (prev or {}).get("by_point") or {}
for p, (_, d) in e["by_point"].items():
points[p] = plus([points[p], minus(d, (before.get(p) or [0, 0.0])[1])])
last[e["session"]] = e
p = prs.setdefault(e["url"], {"date": e.get("date"), "commit": e.get("commit"), "by": [], "cost": 0.0, "coord": None})
if e.get("role", "built") == "built":
Expand All @@ -437,6 +449,9 @@ def history():
+ ("" if p["coord"] is None else money(p["coord"])) + " |")
built, coord = plus(p["cost"] for p in prs.values()), plus(p["coord"] for p in prs.values() if p["coord"] is not None)
out += ["", f"Pull requests: {money(built)}. Coordination: {money(coord)}."]
if points:
out += ["", "**Pull requests by model and effort**, from ck 0.1.27 on", "", "| Model and effort | Cost |", "|---|--:|"]
out += [f"| {p} | {money(d)} |" for p, d in sorted(points.items(), key=lambda i: -(i[1] or 0))]
prices, _ = pricing()
after, sessions = 0.0, 0
for s, e in last.items():
Expand Down
Loading