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": "claude-team",
"description": "Seventeen named specialist personas for Claude Code: session-scoped /name switching, delegation subagents with model tiers, a persona session launcher, and coordinator workflows with branch hygiene.",
"description": "Named specialist personas for Claude Code: session-scoped /name switching, delegation subagents with model tiers, a persona session launcher, and coordinator workflows with branch hygiene.",
"version": "2.0.0",
"author": { "name": "Code Katz" }
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ The `0.4` through `0.7` numbers used during development are retired. v1 is the f
- `claude-team sync` propagates a profile edit to all three installed copies
- `claude-team install-hook` registers session context on the clone install path
- Plain technical English standard for the six coding specialists ([WRITING.md](WRITING.md))
- 175-test suite covering the CLI commands, both coordinator modes, and the install path
- 223-test suite covering the CLI commands, both coordinator modes, and the install path

### v1.0

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ v1 delivered the roster and the coordinator. v2 closes the gaps that made the te
- [x] Slash commands generated from profiles, so a new persona means writing one file instead of three
- [x] Session handoff briefing: every persona defines a Handoff Brief, the coordinator asks for one at a switch, and it now reaches all four delivery surfaces including the `/name` slash commands
- [x] Parallel session prompts carry a Context field, so a session starts from what was already decided instead of re-deriving it
- [x] 175-test suite covering the CLI commands, both coordinator modes, and the install path
- [x] 223-test suite covering the CLI commands, both coordinator modes, and the install path

### Shipped — v1.0

Expand Down
74 changes: 72 additions & 2 deletions bin/claude-team
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,34 @@ get_active() {
# destroy the user's file. The lock covers the whole read-modify-write. Two
# sessions writing different blocks of the same file otherwise lose one of
# them, or each fail to see the other's marker and append a second copy.
# Refuse to rewrite a file whose marker pair is damaged. block_install and
# block_remove both locate the block by scanning for START and END; if END is
# missing, both delete everything from START to end of file, which is the user's
# own content on a file they are told to hand-edit. Reproduced: a CLAUDE.md with
# a lost END marker lost a personal runbook to 'claude-team use', exit 0.
#
# Substring matching is why a bare count is the right check: a line merely
# mentioning the marker inside the user's prose is indistinguishable from the
# marker itself, so two STARTs is equally unsafe to rewrite blind.
block_assert_sane() {
local file="$1" start="$2" end="$3" label="$4" starts ends
[[ -f "$file" ]] || return 0
starts=$(grep -cF "$start" "$file" || true)
ends=$(grep -cF "$end" "$file" || true)
[[ "$starts" == "$ends" && "$starts" -le 1 ]] && return 0
die "The $label markers in $file are damaged: $starts start, $ends end (expected one of each, or none).
Refusing to rewrite, because repairing this automatically would guess at which
of your own lines belong inside the block. Fix the markers by hand, or delete
the block entirely, then rerun. The markers are:
$start
$end"
}

block_install() {
local file="$1" start="$2" end="$3" content="$4"
lock_acquire "$file"
touch "$file"
block_assert_sane "$file" "$start" "$end" "block"
local tmp
tmp=$(tmp_beside "$file")
if grep -qF "$start" "$file"; then
Expand All @@ -324,6 +348,7 @@ block_install() {
block_remove() {
local file="$1" start="$2" end="$3"
lock_acquire "$file"
block_assert_sane "$file" "$start" "$end" "block"
local tmp
tmp=$(tmp_beside "$file")
awk -v s="$start" -v e="$end" '
Expand Down Expand Up @@ -616,6 +641,20 @@ get_active_branch() {

# ─── Branch commands ──────────────────────────────────────────────────────────

# The branch index is a pipe-delimited table read back with awk -F'|', so a
# literal '|' in any field shifts every column after it. Nothing rejected one,
# and git itself accepts a '|' in a ref name, so 'branch start feat/a|b' or a
# plan slug like "sprint-42 | scoped" printed a success line and then wrote a
# row the tool could never read back: status reported none, done could not close
# it, and the row stayed active forever. Rejecting at the two write sites is
# cheaper and safer than making every reader robust to a broken row.
assert_index_safe() {
local value="$1" label="$2"
[[ "$value" != *"|"* ]] || die "The $label may not contain '|': $value
The branch index is a pipe-delimited table, so that character would corrupt the
row and the entry could not be read back. Use a different separator."
}

cmd_branch_start() {
[[ $# -ge 1 ]] || die "Usage: claude-team branch start <branch-name> [--plan <slug>]"
local branch_name="$1"
Expand Down Expand Up @@ -649,6 +688,10 @@ Run 'claude-team branch done' or 'claude-team branch abandon' before starting a

ensure_branches_index

assert_index_safe "$branch_name" "branch name"
assert_index_safe "$plan_slug" "plan slug"
assert_index_safe "$project" "project name"

local today
today=$(date '+%Y-%m-%d')
printf '| %s | %s | %s | %s | active | |\n' \
Expand Down Expand Up @@ -948,6 +991,11 @@ cmd_session_start() {
esac
done

# Fail before creating a worktree, not after: a rejected row here would
# otherwise leave an unregistered worktree on disk.
assert_index_safe "$branch_name" "branch name"
assert_index_safe "$plan_slug" "plan slug"

local repo_root project worktree_path
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) \
|| die "Not in a git repo. Run this from your project's root directory."
Expand Down Expand Up @@ -1057,13 +1105,23 @@ Run this from inside a session worktree, or use 'claude-team branch done' for no
# that had not been git added yet was deleted below with no warning and a
# zero exit. A file you have written but not staged is the most ordinary
# state in a working session, so that was the common case, not an edge one.
# --ignored is not optional. Plain --porcelain omits ignored files, and
# 'git worktree remove' deletes them, so without it a .env holding the only
# copy of a credential was destroyed with a zero exit. A file is gitignored
# because it is generated or because it is secret, and the second kind exists
# nowhere else.
# .claude-session is excluded because this command created it: session start
# writes the marker and adds it to info/exclude, so it shows up as ignored in
# every worktree and would otherwise block every close. It is the one ignored
# path the tool owns and is meant to remove.
local dirty
dirty=$(git -C "$worktree_path" status --porcelain 2>/dev/null)
dirty=$(git -C "$worktree_path" status --porcelain --ignored 2>/dev/null \
| grep -v '^!! \.claude-session$' || true)
if [[ -n "$dirty" ]]; then
echo "$(red "⚠") Worktree still has uncommitted work:" >&2
printf '%s\n' "$dirty" | sed 's/^/ /' >&2
echo " Commit it, stash it, or delete the files, then close the session." >&2
echo " $(dim "Untracked files count: they are not in any commit and would be lost.")" >&2
echo " $(dim "'??' is untracked and '!!' is ignored. Neither is in any commit.")" >&2
exit 1
fi

Expand Down Expand Up @@ -1092,8 +1150,20 @@ Run this from inside a session worktree, or use 'claude-team branch done' for no
print
}
' "$BRANCHES_INDEX" > "$tmp" || die "Failed to rewrite the branch index: $BRANCHES_INDEX"
# An unchanged file means the awk matched no row, so the session is about to
# be closed while the index still calls it active. Say so: silence here is
# what turns a malformed row into a success message and a permanent ghost
# entry. The worktree removal below is still what the user asked for.
# An 'if', not 'cmp && assign': the files differ in the normal case, so the
# && list would return non-zero and set -e would kill the function here.
local index_unchanged=false
if cmp -s "$tmp" "$BRANCHES_INDEX"; then index_unchanged=true; fi
tmp_commit "$tmp" "$BRANCHES_INDEX"
lock_release
if [[ "$index_unchanged" == true ]]; then
echo "$(yellow "!") No active index row matched '$branch' for '$project', so nothing was marked merged." >&2
echo " $(dim "Closing the worktree anyway. Check: claude-team branch list")" >&2
fi

# Remove the worktree (git requires this from outside it). No --force
# fallback: git refuses to remove a worktree holding modified or untracked
Expand Down
10 changes: 5 additions & 5 deletions gtm.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ claude-team-cli is not a prompt library. It's not a collection of system prompts

It's a named, opinionated specialist who shows up with domain expertise, asks the questions a senior practitioner would ask, and pushes back when something's off. Ten of them, covering the full product development lifecycle from discovery to launch.

**One-line positioning:** *Your AI development team. Ten specialists, one CLI, zero meetings.*
**One-line positioning:** *Your AI development team. Seventeen specialists, one CLI, zero meetings.*

### Differentiation Hook

Expand Down Expand Up @@ -206,7 +206,7 @@ The scenario is absurd enough to be memorable and complex enough to be real.

### Meet My Claude Dev Team

**Ten specialists. One CLI. Zero meetings.**
**Seventeen specialists. One CLI. Zero meetings.**

---

Expand Down Expand Up @@ -308,7 +308,7 @@ Switch mid-session without restarting:
/morgan /alex /robin /toni /quinn
```

Works on macOS and Linux. Requires Bash 3.2+ and Claude Code.
Works on macOS and Linux. Requires Bash 4+ and Claude Code. macOS ships Bash 3.2, so run `brew install bash` first.

**Bonus:** Two companion skills — [claude-devlog-skill](https://github.com/code-katz/claude-devlog-skill) and [claude-roadmap-skill](https://github.com/code-katz/claude-roadmap-skill) — give the team persistent memory. The devlog captures architectural decisions across sessions. The roadmap tracks priorities and records every reprioritization. Install both with two commands and your team doesn't just think differently — it *remembers* differently. More on these in a later post.

Expand All @@ -335,7 +335,7 @@ Same product, ten different specialists, and the tools that make sure none of it
### Post Body (paste directly into LinkedIn)

```
I built a 10-person AI dev team inside Claude Code.
I built a 17-person AI dev team inside Claude Code.

Not prompt templates. A CLI that gives Claude named
specialist personas — senior consultants with domain
Expand Down Expand Up @@ -413,7 +413,7 @@ What would you automate first?
```
Repos:

Team CLI (11 specialist personas):
Team CLI (17 specialist personas):
github.com/code-katz/claude-team-cli

Companion skills:
Expand Down
Loading
Loading