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
45 changes: 45 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,51 @@ Auto-maintained via Claude devlog skill. Entries are reverse-chronological.

---

## [2026-07-29] Two stale promises: a customization layer that should not exist, and a feature that already did

**Category:** `decision`
**Tags:** `roadmap`, `docs`, `handoff`, `codegen`, `messaging`, `dogfooding`

**Risk Level:** `low`
**Breaking Change:** `no`

### Summary

PRs #23 and #24 close the same kind of gap from opposite directions: the documentation described a product that did not match the code. #23 removed a promise that should never be kept. #24 found a shipped feature listed as unbuilt, and fixed the one surface where it failed to arrive. The suite went 163 to 175.

### Detail

**Local profile overrides were retired rather than deferred.** They had been the top unbuilt roadmap item since v2 planning. The reasoning that killed them came from the product owner and is worth preserving verbatim in spirit: an override layer creates a second source of persona truth competing with the repo, and no reliable way to tell whether Akira is behaving like upstream Akira or like a local edit. Team-scoped profiles, the same layer at project scope via `claude-team init` and `.claude-team/`, went for the same reason. The supported paths are the two that already existed, a pull request or a fork, and `CONTRIBUTING.md` now documents both along with persona authoring.

**The installer was overstating what it had activated, and the first diagnosis was wrong.** The claim was that it advertised subagents as ready to delegate to. That message had been deleted by `cbbb7a4` when `install.sh` began delegating to `claude-team sync`. The real defect was silence. Claude Code registers subagents and hooks when a session starts and reads slash commands on demand, so a sync activates one of three surfaces immediately. Nothing said so, three green checkmarks in a row read as three live capabilities, and `install.sh` closed with "Your Claude dev team is ready." The SessionStart hook had the starkest form of it, printing a bare checkmark for something that cannot by definition fire in the session that registers it.

**Session handoff briefing was listed as aspirational and had shipped with the personas.** All seventeen carried a Handoff Brief section with byte-identical opening and closing stems; both coordinator profiles instructed the coordinator to ask for one at a switch; three earlier DEVLOG entries treat "3 behaviors + handoff" as a structural invariant. The roadmap entry had been written by paraphrasing the already-shipped `robin.md` text.

**The defect was delivery, and it landed on the worst surface available.** `### Handoff Brief` sat inside `## Required Interactive Behaviors`, which `strip_interactive_behaviors` excises from every slash command: 17 of 17 subagents carried it, 0 of 17 slash commands did. `coordinator.md:126` tells the user to run `/<name>` at the exact moment of a handoff, and `:128` then tells the coordinator to ask that persona for a brief. The asker had the instruction. The answerer, on the path the coordinator itself recommends, had never been told what a Handoff Brief is.

**A test asserted the bug as correct.** The commands drift check mirrored the stripper's awk exactly, so the absence was not an oversight that slipped through review; it was pinned in place by CI. This is the third defect in two days where a green suite protected the wrong behavior, after the hook tests that passed while the hook never installed, and the three-copy persona problem that produced no error.

### Decisions Made

- **The heading was promoted rather than the stripper made cleverer.** `### Handoff Brief` became `## Handoff Brief`, and the stripper's end marker moved from `## Signature Question` to it. This codifies what the structure already implied: the section was deliberately unnumbered while its siblings are `### 1.`, `### 2.`, `### 3.`
- **The generator now refuses a profile with no `## Handoff Brief`.** The stripper resets its skip flag on that marker, so a profile lacking it would silently drop every later section from the slash command. The identical latent failure exists with `## Signature Question` and goes unnoticed only because all seventeen happen to have one.
- **Handoffs stay prompt-only.** No file, no command, no stored state. This keeps the feature clear of the shared-state locking the branch index needed, and it was the product owner's explicit call.
- **Parallel session prompts gained a fourth Context field.** They carried Persona, Task, and File scope, so every session re-derived context it was never given. A finished session with dependents now writes a brief that becomes the downstream Context.
- **Dated history is never rewritten.** The roadmap row promoting overrides to top priority stays verbatim; new rows record the retirement and the correction.

### Mistakes Worth Recording

- **A control test passed for the wrong reason.** Proving the traversal tests were meaningful required running them against pre-fix code, but the scratch tree was missing `gtm.md`, so the traversal resolved to nothing and every assertion passed against unfixed code. The suite now asserts the traversal target exists before testing against it. The same mistake nearly recurred on the handoff control.
- **The installer diagnosis was wrong in its specifics** and was corrected in the PR body rather than quietly restated. The message being blamed had been deleted three commits earlier.

### Related

- PR #23, PR #24
- `CONTRIBUTING.md`: the supported customization paths and persona authoring
- ROADMAP: the aspirational list is now empty, two items retired and one found already shipped

---

## [2026-07-29] v2 hardening: four specialists, a real race, and a lint I broke

**Category:** `milestone`
Expand Down
101 changes: 66 additions & 35 deletions bin/claude-team
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@ _lock_owner() { cat "$1/owner" 2>/dev/null || true; }
_lock_is_stale() {
local lock_dir="$1" host pid started now
[[ -d "$lock_dir" ]] || return 1
if ! read -r host pid started < "$lock_dir/owner" 2>/dev/null; then
# The redirect must precede the command. Bash applies redirections left to
# right and aborts on the first failure, so a trailing 2>/dev/null is never
# installed when the '<' itself fails. A missing owner file is the normal
# transient state described above, so the old form leaked a raw bash error to
# the terminal on roughly half of all contended calls.
if ! 2>/dev/null read -r host pid started < "$lock_dir/owner"; then
return 1
fi
now=$(date '+%s')
Expand Down Expand Up @@ -237,6 +242,35 @@ title_name() {
printf '%s\n' "${1%% —*}"
}

# Drop a trailing "Consultant" or "Manager", which reads as noise in a one-line
# roster, unless dropping it would leave a single word: "Product Manager" must
# not become "Product". Mirrors the rule in scripts/generate-agents.sh.
short_role() {
local role="$1"
case "$role" in
*" Consultant" | *" Manager")
case "${role% *}" in *" "*) role="${role% *}" ;; esac
;;
esac
printf '%s\n' "$role"
}

# Emit "slug<TAB>Display<TAB>Short role" for every persona, read from the
# profiles on disk. Every roster the CLI prints derives from this. Four separate
# hardcoded copies of this list had drifted, leaving 'claude-team help' and the
# installer missing four to nine of the seventeen personas: a roster a human has
# to remember to update is a roster that goes stale.
persona_roster() {
local profile name title
for profile in "$PROFILES_DIR"/*.md; do
[[ -f "$profile" ]] || continue
name=$(basename "$profile" .md)
case "$name" in coordinator*) continue ;; esac
title=$(profile_title "$profile")
printf '%s\t%s\t%s\n' "$name" "$(title_name "$title")" "$(short_role "$(title_role "$title")")"
done
}

title_role() {
printf '%s\n' "${1#*— }"
}
Expand Down Expand Up @@ -522,19 +556,10 @@ Make sure you are running this from the claude-team-cli repo."
done
echo ""
echo "Use them in any Claude Code session:"
echo " $(dim "/robin") — switch to Robin (Testing)"
echo " $(dim "/akira") — switch to Akira (Backend)"
echo " $(dim "/sasha") — switch to Sasha (Frontend)"
echo " $(dim "/toni") — switch to Toni (Product Marketing)"
echo " $(dim "/river") — switch to River (Product)"
echo " $(dim "/alex") — switch to Alex (DevOps)"
echo " $(dim "/morgan") — switch to Morgan (Security)"
echo " $(dim "/jordan") — switch to Jordan (Data & ML)"
echo " $(dim "/casey") — switch to Casey (Data Analyst)"
echo " $(dim "/quinn") — switch to Quinn (Project Manager)"
echo " $(dim "/sage") — switch to Sage (Business Advisor)"
echo " $(dim "/kai") — switch to Kai (UX Design)"
echo " $(dim "/iris") — switch to Iris (Brand & Illustration)"
# Same derivation as cmd_help: one roster, read from the profiles on disk.
while IFS=$'\t' read -r slug display role; do
printf ' %s — switch to %s (%s)\n' "$(dim "/$slug")" "$display" "$role"
done < <(persona_roster)
echo " $(dim "/team") — show current team status"
echo ""
echo "$(dim "No new session required — switches take effect immediately.")"
Expand Down Expand Up @@ -1026,11 +1051,19 @@ Run this from inside a session worktree, or use 'claude-team branch done' for no
project=$(grep '^project=' "$worktree_path/.claude-session" | cut -d= -f2)
branch=$(grep '^branch=' "$worktree_path/.claude-session" | cut -d= -f2)

# Warn on uncommitted changes
if ! git -C "$worktree_path" diff --quiet 2>/dev/null || \
! git -C "$worktree_path" diff --cached --quiet 2>/dev/null; then
echo "$(red "⚠") Uncommitted changes in worktree." >&2
echo " Commit or stash before closing the session." >&2
# Refuse to close a worktree that still holds work. git status --porcelain
# reports staged, unstaged, AND untracked in one pass. The previous check used
# two git diff calls, neither of which sees an untracked file, so a new file
# 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.
local dirty
dirty=$(git -C "$worktree_path" status --porcelain 2>/dev/null)
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
exit 1
fi

Expand Down Expand Up @@ -1062,9 +1095,15 @@ Run this from inside a session worktree, or use 'claude-team branch done' for no
tmp_commit "$tmp" "$BRANCHES_INDEX"
lock_release

# Remove worktree (must be called from outside the worktree)
git -C "$repo_root" worktree remove "$worktree_path" 2>/dev/null \
|| git -C "$repo_root" worktree remove "$worktree_path" --force
# Remove the worktree (git requires this from outside it). No --force
# fallback: git refuses to remove a worktree holding modified or untracked
# files, which is the same protection as the guard above, and retrying with
# --force overrode it unconditionally. Anything git blocks here is a state the
# guard should have caught, so surface it instead of forcing through it.
git -C "$repo_root" worktree remove "$worktree_path" \
|| die "git refused to remove the worktree at $worktree_path.
The branch index already records this session as merged. Inspect the worktree,
then remove it with: git -C $repo_root worktree remove --force $worktree_path"

echo ""
echo "$(green "✓") Session closed: $(bold "$branch")"
Expand Down Expand Up @@ -1291,19 +1330,11 @@ cmd_help() {
echo " claude-team branch guard remove Remove the pre-commit hook"
echo ""
echo "$(bold "Examples:")"
echo " claude-team use robin Activate Robin (Testing)"
echo " claude-team use akira Activate Akira (Backend)"
echo " claude-team use sasha Activate Sasha (Frontend)"
echo " claude-team use toni Activate Toni (Product Marketing)"
echo " claude-team use river Activate River (Product)"
echo " claude-team use alex Activate Alex (DevOps)"
echo " claude-team use morgan Activate Morgan (Security)"
echo " claude-team use jordan Activate Jordan (Data & ML)"
echo " claude-team use casey Activate Casey (Data Analyst)"
echo " claude-team use quinn Activate Quinn (Project Manager)"
echo " claude-team use sage Activate Sage (Business Advisor)"
echo " claude-team use kai Activate Kai (UX Design)"
echo " claude-team use iris Activate Iris (Brand & Illustration)"
# Derived from the profiles on disk, so a new persona appears here the moment
# its profile lands. The previous hardcoded list was missing four of them.
while IFS=$'\t' read -r slug display role; do
printf ' claude-team use %-22s Activate %s (%s)\n' "$slug" "$display" "$role"
done < <(persona_roster)
echo " claude-team coordinator on Enable coordinator (casual — commit to main, no branch gate)"
echo " claude-team coordinator prod Enable coordinator (prod — branch required before code)"
echo " claude-team reset Return to default Claude behavior"
Expand Down
20 changes: 11 additions & 9 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,7 @@ echo " SessionStart hook, and the coordinator. Slash commands work right now."
echo ""
echo "Quick start:"
echo " claude-team list $(dim "# see your team")"
echo " claude-team use robin $(dim "# activate Robin (Testing)")"
echo " claude-team use akira $(dim "# activate Akira (Backend)")"
echo " claude-team use sasha $(dim "# activate Sasha (Frontend)")"
echo " claude-team use toni $(dim "# activate Toni (Product Marketing)")"
echo " claude-team use river $(dim "# activate River (Product)")"
echo " claude-team use sage $(dim "# activate Sage (Business Advisor)")"
echo " claude-team use kai $(dim "# activate Kai (UX Design)")"
echo " claude-team use iris $(dim "# activate Iris (Brand & Illustration)")"
echo " claude-team use <name> $(dim "# activate a team member for all new sessions")"
echo " claude-team coordinator on $(dim "# casual mode (commit to main, no branch enforcement)")"
echo " claude-team coordinator prod $(dim "# prod mode (branch required before code)")"
echo " claude-team coordinator off $(dim "# disable coordinator")"
Expand All @@ -153,5 +146,14 @@ echo " claude-team branch abandon $(dim "# mark abandoned")"
echo " claude-team branch guard install $(dim "# block accidental commits on main")"
echo ""
echo "Slash commands $(dim "(switch personas mid-session, no restart needed)"):"
echo " /robin /akira /sasha /toni /river /sage /kai /iris /team /branch /session"
# Derived from the profiles just installed. The hardcoded line this replaces
# listed 8 of the 17 personas, so more than half the team was undiscoverable
# from the installer's own output.
printf ' '
for _p in "$REPO_DIR"/profiles/*.md; do
_n=$(basename "$_p" .md)
case "$_n" in coordinator*) continue ;; esac
printf '/%s ' "$_n"
done
printf '\n /team /branch /session /parallel\n'
echo ""
Loading
Loading