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
4 changes: 2 additions & 2 deletions .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), and the full persona roster, a complete cross-domain team, as ck:<name> subagents and /ck:<name> switch commands.",
"version": "0.1.19",
"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.20",
"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-10-30] 0.1.20: a team per project

**Category:** `feature`
**Tags:** `ck`, `team`, `personas`, `casting`
**Risk Level:** `low`
**Breaking Change:** `no` (a project with no team file now casts from the core; any seat is one invite away)

### Summary
Each project's ck team is the core, eight seats (River, Quinn, Akira, Sasha, Kai, Robin, Alex, Morgan), plus the changes in `.claude/ck-team.conf`: one `invite <persona> <why>` or `drop <persona> <why>` per line. Every session, `/ck:do`, `/ck:opportunity`, `/ck:plan`, and `/ck:start`'s cards keep to it, so d20mob works with Reiner, Tracy, Travolta, and Piper and is never offered Noon. This rebuilds on main the proposal approved on 2026-10-16 (D1 to D4), re-scoped against 0.1.19 on a proof Will reviewed on 2026-10-28, where he set the goals: ck **never casts** a persona off the team (G1); a plan row naming one gets a note and **no card** (G2); a project with **no file has the core** (G3); and the full `/ck:team` invites its cast onto the team when it finishes (G4).

### Detail
- **`scripts/team.py`**, the one reader and writer: `show`, `roster` (the roster rows of the team only), `context` (the session paragraph), `invite`, `drop`. The last line about a persona wins; `invite` and `drop` keep only what differs from the core, so inviting a dropped core seat or dropping an invited guest removes its line. The project is the nearest folder up with the file or `.git`. The core is a list in the script, not a second file.
- **Every session**: the SessionStart hook runs `team.py context`, one paragraph naming the team (about 70 tokens).
- **Casting**: `do.sh look` prints `roster:` lines for the team only, and `/ck:do` takes the nearest team seat when a default owner (Iris, Toni) is off it. The opportunity and plan workflows read `team.py roster` instead of the whole roster; a seat a prompt fixes (Toni and Akira in the opportunity) still runs (D4).
- **Plans**: `plan.py check` adds a `note:` line for an owner off the team, and `plan.py card` refuses that row until the persona is invited; `/ck:start` relays the refusal as it is, so `skills/start/` and the Who column are unchanged for lanes. Outside any repository with no team file, `plan.py` checks names against the roster only.
- **`/ck:team show | invite | drop`** run the script, offer to commit the file, and launch nothing. The full `/ck:team` ends by inviting each Cast seat of `docs/TEAM.md` that is off the team.
- **Cut from the proposal**: the warning line in the 21 switch commands (C7; `/ck:noon` still works everywhere), `core.conf`, `team.py has`, the "Invite Noon?" question mid-workflow, and changes to `team-draft.js` and `roadmap-draft.js`.
- **Tests**: section 23, ten cases on `team.py` and `plan.py` (no file, d20mob's file, an unknown name, invite and drop, a persona both invited and dropped, the paragraph, the hook, the note and the refusal); the plan fixture project carries d20mob's team file. 495 checks pass.

### Open
- Not yet tried in a real session. The four checks to try in d20mob are in the pull request.
- A guest (one use, asked once) leaves no line, as the proposal assumed; one core for every kind of project.

---

## [2026-10-11] 0.1.10: /ck:do, the quick path; /ck:plan and /ck:start, a staged plan and one named session per stage

**Category:** `feature`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Phase one, first release in progress. What runs today:
| The memory gate | Not a command: it runs on its own. Before a session saves anything to memory, it shows you the text, whether it belongs in memory or in the project's `CLAUDE.md` and why, and any memory or `CLAUDE.md` line that already says it or says the opposite. You pick save to memory, add to `CLAUDE.md` (written at once on a branch with a draft pull request), or don't save. A hook makes Claude Code ask you before every write to a memory folder, in auto mode too | Built; not yet run on a real project |
| `/ck:panel <question>` | River, Toni, and Kai argue one question on three different models; a memo shows where they disagree | Built |
| `/ck:prd [--panel]` | River drafts `docs/PRD.md` from the brief, a checker validates, River adds a premortem; with `--panel`, three specialists on three models challenge it first and River rewrites; you review on a page you can comment on | Built; drilled 2026-10-05 on a real project |
| `/ck:team` | Who is on this product, who owns what, which seat is missing: `docs/TEAM.md`; you review on a page you can comment on | Built; drilled 2026-10-05 on a real project |
| `/ck:team [show \| invite <persona> [why] \| drop <persona> [why]]` | This project's team, kept in `.claude/ck-team.conf`: the core (River, Quinn, Akira, Sasha, Kai, Robin, Alex, Morgan) plus whoever is invited; ck suggests and casts only these seats. `show`, `invite`, and `drop` change the file and nothing else. With nothing after it: who is on this product, who owns what, which seat is missing, as `docs/TEAM.md`, reviewed on a page you can comment on; its cast is then invited onto the team | Built; drilled 2026-10-05 on a real project; the team file is new in 0.1.20 |
| `/ck:architecture [--panel]` | Akira recommends an architecture from the PRD and roadmap and adds a premortem; with `--panel`, Morgan, Alex, and Jordan challenge it first on three models; you review on a page you can comment on | Built; drilled 2026-10-05 on a real project |
| `/ck:opportunity <idea>` | River frames the idea, Toni, Akira, and a domain seat write sourced sections, River assembles `docs/opportunity.md`; you review on a page you can comment on | Built; drilled 2026-10-01 |
| `/ck:market-research [focus]` | Toni plans the questions, researchers answer them from the web in parallel, a cross-check marks what is unsourced or contradictory, Toni writes `docs/market-research.md` | Built; drilled 2026-10-01 |
Expand Down
5 changes: 3 additions & 2 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"description": "ck: persona usage log, a check that the old team tool is gone, the memory gate, and the session cost (a line after each answer once there is a pull request, and the table on the pull request at merge)",
"description": "ck: persona usage log, a check that the old team tool is gone, the memory gate, the project's team, and the session cost (a line after each answer once there is a pull request, and the table on the pull request at merge)",
"hooks": {
"SubagentStart": [
{
Expand All @@ -13,7 +13,8 @@
{
"hooks": [
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/check-prereqs.sh\"" },
{ "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/memory-gate.py\" rule" }
{ "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/memory-gate.py\" rule" },
{ "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/team.py\" context" }
]
}
],
Expand Down
6 changes: 3 additions & 3 deletions scripts/do.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ look)
echo "root: $(git rev-parse --show-toplevel 2>/dev/null)"
echo "branch: $(git branch --show-current 2>/dev/null)"
echo "uncommitted: $(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
# The roster, one persona per line: name, role, and tier (fable, opus, sonnet).
awk -F' [|] ' '/^[|] [a-z]+ [|]/ { sub(/^[|] /, ""); split($3, m, "-"); print "roster: " $1 ", " $2 ", " m[2] }' \
"$(dirname "$0")/../profiles/ROSTER.md"
# This project's team, one persona per line: name, role, and tier (fable, opus, sonnet).
python3 "$(dirname "$0")/team.py" roster |
awk -F' [|] ' '/^[|] [a-z]+ [|]/ { sub(/^[|] /, ""); split($3, m, "-"); print "roster: " $1 ", " $2 ", " m[2] }'
for p in CLAUDE.md docs/TEAM.md docs/PRD.md docs/brand-guide.md docs/design; do
if [ -e "$p" ]; then echo "$p"; fi
done
Expand Down
23 changes: 23 additions & 0 deletions scripts/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
import re
import sys

try:
import team # scripts/team.py, beside this script
except ImportError:
team = None

SECTIONS = ['What this covers', 'Phases', 'Stage by stage', 'Open points']
LABELS = ['Kind', 'Does', 'Files', 'Reads', 'Follows', 'Done when', 'Hands back', 'With', 'Serves']
REQUIRED = LABELS[:7]
Expand All @@ -41,6 +46,15 @@
WORD_LIMIT = 3500
ROSTER = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'profiles', 'ROSTER.md')


def off_team(path):
"""A test of a persona name: true when the plan's project has a team and the name is not on it."""
start = os.path.dirname(os.path.abspath(path))
if team is None or team.project(start) is None:
return lambda name: False
members = team.team(start)
return lambda name: name.lower() not in members

# Numbers are written without padding (0, 7, 12, never 07) and are capped at six digits so no
# input can make int() fail. No pattern here ends in a lazy run followed by optional spaces:
# under fullmatch that takes time that grows with the square of a long line.
Expand Down Expand Up @@ -724,6 +738,12 @@ def cmd_check(args, opts):
pending = plan.pending(row)
lines.append('note: %s is %s, but %s, which it follows, %s not done'
% (row.stage, row.progress, plan.waits(row), 'is' if len(pending) == 1 else 'are'))
outside = off_team(args[0])
for row in plan.rows:
who = who_parts(row.who)
if who and outside(who[0]):
lines.append('note: %s: %s is not on this project\'s team; /ck:team invite %s adds them'
% (tag(row), who[0], who[0].lower()))
if roster is None:
lines.append('roster: not found, names not checked')
return status, lines + ['words: %d' % plan.words]
Expand Down Expand Up @@ -882,6 +902,9 @@ def cmd_card(args, opts):
return 1, ['%s has no entry under Stage by stage' % full]
if not who_parts(row.who):
return 1, ['%s has no persona and model the plan allows: %s' % (full, row.who)]
if off_team(path)(row.owner[0]):
return 1, ['%s: %s is not on this project\'s team. Type /ck:team invite %s, or give the row another owner '
'with /ck:plan' % (full, row.owner[0], row.owner[0].lower())]
if not entry.title:
return 1, ['%s has an entry with no title, so its session has no name' % full]
if row.progress == 'unknown':
Expand Down
133 changes: 133 additions & 0 deletions scripts/team.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""team.py: this project's ck team, the core below plus the changes in <project>/.claude/ck-team.conf.

Usage:
team.py show the team, who is invited or dropped and why, and who is not on it
team.py roster the roster's rows for the team only
team.py context SessionStart: one paragraph naming the team
team.py invite <persona> [why] put a persona on the team
team.py drop <persona> [why] take a persona off the team

The file holds one line per change to the core, `invite <persona> <why>` or `drop <persona> <why>`, and
`#` comments. The last line about a persona wins. With no file the team is the core. The project is the
nearest folder, from the current one up, that has .claude/ck-team.conf or .git. Standard library only;
an error is one plain line, never a traceback.
"""
import os
import re
import sys
from pathlib import Path

ROSTER = Path(__file__).resolve().parent.parent / 'profiles' / 'ROSTER.md'
CORE = ['river', 'quinn', 'akira', 'sasha', 'kai', 'robin', 'alex', 'morgan']
HEAD = '# ck team. The core comes from the plugin; this file invites and drops. Change it with /ck:team invite or drop.'
VERBS = ('invite', 'drop')


class Fail(Exception):
pass


def roster():
"""Persona name to its row in profiles/ROSTER.md, in the roster's order."""
rows = {}
for line in ROSTER.read_text().splitlines():
m = re.match(r'\| ([a-z]+) \|', line)
if m:
rows[m.group(1)] = line
return rows


def project(start=None):
"""The project's folder, or None outside any repository with no team file."""
here = Path(start or os.getcwd()).resolve()
for d in [here, *here.parents]:
if (d / '.claude' / 'ck-team.conf').is_file() or (d / '.git').exists():
return d
return None


def team_file(start=None):
return (project(start) or Path(start or os.getcwd()).resolve()) / '.claude' / 'ck-team.conf'


def changes(path):
"""Persona to (verb, why, line number), from the last line about each persona."""
found = {}
if path.is_file():
for n, line in enumerate(path.read_text().splitlines(), 1):
words = line.split(None, 2)
if len(words) > 1 and words[0] in VERBS:
found[words[1].lower()] = (words[0], words[2] if len(words) > 2 else '', n)
return found


def team(start=None):
"""The team's persona names: the core that is not dropped, then those invited."""
names, found = roster(), changes(team_file(start))
members = [p for p in CORE if found.get(p, ('invite',))[0] == 'invite']
return members + [p for p, c in found.items() if c[0] == 'invite' and p not in CORE and p in names]


def show():
names, path = roster(), team_file()
found, members = changes(path), team()
out = ['team: ' + ', '.join(members)]
for verb in VERBS:
said = ['%s%s' % (p, ' (%s)' % c[1] if c[1] else '') for p, c in found.items() if c[0] == verb and p in names]
if said:
out.append('%s: %s' % ('invited' if verb == 'invite' else 'dropped', ', '.join(said)))
out.append('not on the team: ' + (', '.join(p for p in names if p not in members) or 'nobody'))
out.append('file: .claude/ck-team.conf' if path.is_file() else 'file: none, so the team is the core')
out += ['note: line %d names %s, who is not on the ck roster, so it is skipped' % (c[2], p)
for p, c in found.items() if p not in names]
return out


def context():
names = ', '.join(p.capitalize() for p in team())
return [("ck team for this project: %s. Suggest and cast only these seats. Before using anyone else, ask once; "
"/ck:team invite <name> adds a lasting member. A pipeline command's own fixed seats always run." % names)]


def edit(verb, name, why):
name = name.lower()
if name not in roster():
raise Fail('%s is not on the ck roster.' % name)
path = team_file()
before = name in team()
lines = path.read_text().splitlines() if path.is_file() else [HEAD]
lines = [l for l in lines if not (len(l.split()) > 1 and l.split()[0] in VERBS and l.split()[1].lower() == name)]
if (verb == 'invite') != (name in CORE): # the file holds only what differs from the core
lines.append(('%s %s %s' % (verb, name, why)).rstrip())
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('\n'.join(lines) + '\n')
after = name in team()
word = 'on' if after else 'off'
said = '%s is %s this project\'s team.' % (name, ('now ' if before != after else 'already ') + word)
return [said] + show()


def main(argv):
cmd, args = (argv[0] if argv else ''), argv[1:]
if cmd in ('show', 'roster', 'context') and not args:
if cmd == 'show':
return show()
if cmd == 'context':
return context()
rows = roster()
return ['| Persona | Role | Tier model | Domain |', '|---|---|---|---|'] + [rows[p] for p in rows if p in team()]
if cmd in VERBS and args:
return edit(cmd, args[0], ' '.join(args[1:]))
raise Fail('usage: team.py show | roster | context | invite <persona> [why] | drop <persona> [why]')


if __name__ == '__main__':
try:
print('\n'.join(main(sys.argv[1:])))
except Fail as e:
print(e, file=sys.stderr)
sys.exit(1)
except Exception as e: # a hook must never fail a session
print('team.py: %s' % e, file=sys.stderr)
sys.exit(0 if sys.argv[1:2] == ['context'] else 2)
2 changes: 2 additions & 0 deletions skills/do/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ At most three steps, in this order. The owner is a roster persona the author nam
- Build: Sasha for screens, Akira for server and data, Alex for infrastructure. When the author asked for the change made.
- Check: Robin; Morgan when sign-in, payments, or personal data are touched. When asked for, or one of those is touched.

A default owner must have a `roster:` line, since those are this project's team; when one does not, take the nearest seat that does and add "<name> is not on this team; `/ck:team invite <name>` adds them." above "Go?".

Name each step with a verb, the work item's id if given, at most six words, and the owner, as in `Build: footer typo fix (Sasha)`, and use that name everywhere. Send the plan in this shape only, then stop:

```
Expand Down
Loading