diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7962536..ed09a83 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ { "name": "agent-team", "description": "Orchestrates parallel work via Agent Teams with automated coordination, workspace tracking, and hook enforcement", - "version": "1.4.0", + "version": "2.0.0", "source": { "source": "url", "url": "https://github.com/ducdmdev/agent-team-plugin.git" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 4016d73..65544ad 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agent-team", "description": "Orchestrates parallel work via Agent Teams with automated coordination, workspace tracking, and hook enforcement", - "version": "1.4.0", + "version": "2.0.0", "author": { "name": "Duc Do" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f013d7..dd39f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0] - 2026-03-01 + +### Added +- **Git worktree isolation** (opt-in) — `isolation: worktree` in Phase 2 plan gives each implementer a dedicated worktree +- **Nested task decomposition** — senior implementers can create sub-tasks and spawn sub-agents +- Worktree setup and merge scripts (`scripts/setup-worktree.sh`, `scripts/merge-worktrees.sh`) + +### Changed +- Major version bump: nested decomposition changes the team coordination model + +## [1.6.0] - 2026-03-01 + +### Added +- Auto-branch per teammate — implementers create `{team-name}/{name}` branches, merged in Phase 5 +- `events.log` workspace file — structured JSON event log for post-mortem analysis +- Direct Handoff coordination pattern — authorized peer-to-peer messaging with audit trail +- Branch Merge step in Phase 5 + +## [1.5.0] - 2026-03-01 + +### Added +- **SessionStart(compact) hook** — auto-recovers workspace context after compaction +- **PreToolUse(Write|Edit) hook** — enforces file ownership (warn-then-block) +- **SubagentStart/SubagentStop hooks** — tracks teammate lifecycle in events.log +- `file-locks.json` workspace file — maps teammates to owned files/directories + +### Changed +- TaskCompleted hook now uses `task_id` and `teammate_name` for scoped git checks +- Hooks section in SKILL.md updated to document all 5 hooks + ## [1.4.0] - 2026-02-28 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 62d1c83..c200518 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,12 +28,17 @@ docs/ Reference docs consumed by SKILL.md at runtime |------|---------|----------------| | `.claude-plugin/plugin.json` | Plugin identity | Bump version here on release | | `.claude-plugin/marketplace.json` | Marketplace registry | Bump version here too, keep in sync with plugin.json | -| `hooks/hooks.json` | Hook registration | Update timeout values, add new hooks, or update hook command paths | -| `scripts/*.sh` | Hook enforcement logic | Written in bash (`#!/bin/bash`), degrade gracefully without `jq` | +| `hooks/hooks.json` | Hook registration (6 hooks) | Update timeout values, add new hooks, or update hook command paths | +| `scripts/*.sh` | Hook enforcement logic (7 scripts) | Written in bash (`#!/bin/bash`), degrade gracefully without `jq` | | `skills/agent-team/SKILL.md` | Core skill prompt | Most changes go here. Keep Phase 1-5 structure | | `docs/worker-roles.md` | Role definitions + spawn templates | Update when adding new roles | | `docs/coordination-patterns.md` | Conflict resolution, handoffs | Update when adding new coordination patterns | +| `docs/workspace-templates.md` | Workspace file templates | Update when adding new workspace files | | `docs/report-format.md` | Final report template | Update when changing report structure | +| `docs/custom-roles.md` | Project-specific role template | Reference for users creating custom roles | +| `CHANGELOG.md` | Version history | Add entry for each release | +| `README.md` | User-facing documentation | Keep in sync with feature changes | +| `tests/` | Hook and structure tests | `hooks/` for hook tests, `structure/` for plugin validation | ## Conventions @@ -71,6 +76,14 @@ chore: maintenance (package.json, CI, dependencies) ## Testing +### Run Full Test Suite + +```bash +bash tests/run-tests.sh +``` + +Runs 9 test files (78 assertions) covering all hooks and plugin structure. + ### Validate Plugin ```bash @@ -87,9 +100,14 @@ Then trigger with: "use agent team to [task]" ### Verify Hooks -1. Start a team session -2. Try marking a task complete without file changes — TaskCompleted hook should block -3. Let a teammate go idle with in-progress tasks — TeammateIdle hook should nudge +Six hooks registered in `hooks/hooks.json`: + +1. **TaskCompleted** — try marking a task complete without file changes (should block) +2. **TeammateIdle** — let a teammate go idle with in-progress tasks (should nudge) +3. **SessionStart(compact)** — compact context in a team session (should recover workspace) +4. **PreToolUse(Write|Edit)** — have a teammate edit another's file (should warn, then block) +5. **SubagentStart** — spawn a teammate (should log to events.log) +6. **SubagentStop** — teammate shuts down (should log to events.log) ## Common Tasks @@ -109,9 +127,11 @@ Then trigger with: "use agent team to [task]" ### Releasing a New Version -1. Update version in `.claude-plugin/plugin.json` -2. Update version in `.claude-plugin/marketplace.json` -3. Update version in `package.json` -4. Run `claude plugin validate .` -5. Commit with `chore: bump version to X.Y.Z` -6. Tag with `git tag vX.Y.Z` +1. Run `bash tests/run-tests.sh` — all tests must pass +2. Update version in `.claude-plugin/plugin.json` +3. Update version in `.claude-plugin/marketplace.json` +4. Update version in `package.json` +5. Add entry to `CHANGELOG.md` +6. Run `claude plugin validate .` +7. Commit with `chore: bump version to X.Y.Z` +8. Tag with `git tag vX.Y.Z` diff --git a/README.md b/README.md index 0dff43e..5e8d3b4 100644 --- a/README.md +++ b/README.md @@ -99,13 +99,14 @@ QUESTION: what I need to know ## Hooks -Two hooks enforce team discipline automatically: +Five hooks enforce team discipline automatically: ### TaskCompleted Blocks premature task completion by checking: - Workspace exists with all tracking files (`progress.md`, `tasks.md`, `issues.md`) - Implementation tasks have actual file changes (via `git status`) +- Supports scoped checks using `task_id` and `teammate_name` ### TeammateIdle @@ -113,7 +114,26 @@ Nudges idle teammates that still have in-progress tasks: - Counts assigned in-progress tasks - Loop protection: allows idle after 3 consecutive blocks (teammate is genuinely stuck) -Both hooks degrade gracefully — exit 0 if `jq` is missing. +### SessionStart (compact) + +Auto-recovers workspace context after context compaction: +- Detects active workspaces and injects recovery context +- Skips completed workspaces (status: done) + +### PreToolUse (Write|Edit) + +Enforces file ownership boundaries: +- Reads `file-locks.json` from the workspace to determine ownership +- First violation: warns (exit 0). Second violation: blocks (exit 2) +- Workspace files are always allowed regardless of ownership + +### SubagentStart / SubagentStop + +Tracks teammate lifecycle in `events.log`: +- Logs spawn and stop events with timestamps and teammate metadata +- Provides post-mortem analysis data + +All hooks degrade gracefully — exit 0 if `jq` is missing. ## Workspace @@ -121,10 +141,12 @@ Each team creates a persistent workspace at `.agent-team/{team-name}/` in your p ``` .agent-team/{team-name}/ -├── progress.md # Team status, members, decisions, handoffs -├── tasks.md # Task ledger with status and dependencies -├── issues.md # Issue tracker with severity and resolution -└── report.md # Final report (generated at completion) +├── progress.md # Team status, members, decisions, handoffs +├── tasks.md # Task ledger with status and dependencies +├── issues.md # Issue tracker with severity and resolution +├── file-locks.json # File ownership map (teammate -> files/directories) +├── events.log # Structured JSON event log for post-mortem analysis +└── report.md # Final report (generated at completion) ``` - **Persists** after team deletion — it's the permanent record @@ -141,15 +163,22 @@ agent-team-plugin/ ├── hooks/ │ └── hooks.json # Hook definitions (${CLAUDE_PLUGIN_ROOT} paths) ├── scripts/ -│ ├── verify-task-complete.sh # TaskCompleted hook -│ └── check-teammate-idle.sh # TeammateIdle hook +│ ├── verify-task-complete.sh # TaskCompleted hook +│ ├── check-teammate-idle.sh # TeammateIdle hook +│ ├── recover-context.sh # SessionStart(compact) hook +│ ├── check-file-ownership.sh # PreToolUse(Write|Edit) hook +│ ├── track-teammate-lifecycle.sh # SubagentStart/Stop hook +│ ├── setup-worktree.sh # Worktree creation for isolation mode +│ └── merge-worktrees.sh # Worktree merge in Phase 5 ├── skills/ │ └── agent-team/ │ └── SKILL.md # Main skill (team lead orchestrator) ├── docs/ │ ├── worker-roles.md # Role definitions and spawn templates │ ├── coordination-patterns.md # Conflict resolution and handoff patterns -│ └── report-format.md # Final report specification +│ ├── workspace-templates.md # Workspace file templates for Phase 3 +│ ├── report-format.md # Final report specification +│ └── custom-roles.md # Template for project-specific roles ├── package.json ├── CLAUDE.md ├── LICENSE @@ -204,9 +233,9 @@ scoop install jq # Windows For teams larger than 4, verify: (1) every stream has zero file overlap, (2) cross-communication is minimal, (3) workspace churn is manageable. -## Planned Features +## Changelog -See `docs/plans/` for approved designs and implementation plans for upcoming features. +See [CHANGELOG.md](CHANGELOG.md) for a detailed version history. ## License diff --git a/docs/coordination-patterns.md b/docs/coordination-patterns.md index 5b290e2..a36f3ee 100644 --- a/docs/coordination-patterns.md +++ b/docs/coordination-patterns.md @@ -22,6 +22,7 @@ Patterns for the lead to handle common coordination scenarios. - [Adversarial Review Rounds](#adversarial-review-rounds) — multi-round cross-review for critical changes - [Quality Gate](#quality-gate) — final validation pass before synthesis - [Auto-Block on Repeated Failures](#auto-block-on-repeated-failures) — escalation after repeated failures +- [Direct Handoff](#direct-handoff) — authorized peer-to-peer messaging with audit trail ## Communication Protocol @@ -398,3 +399,30 @@ When processing a BLOCKED message: 3. If count < 2: a. Acknowledge and route to resolution as normal ``` + +## Direct Handoff + +For pre-approved information transfers between specific teammates, bypassing the lead for efficiency. + +### When to Use + +- Two teammates have a clear dependency (A produces -> B consumes) +- The handoff content is straightforward (file paths, interface definitions) +- The lead has explicitly authorized the direct channel in their spawn prompts + +### When NOT to Use + +- The handoff requires interpretation or decision-making (route through lead) +- The information needs to be visible to multiple teammates (use lead routing) +- First-time handoffs between teammates who haven't worked together in this session + +### Protocol + +1. **Lead authorizes** in spawn prompts: "For handoffs to [teammate-name], you may message them directly. Include the lead in a summary." +2. **Sender** messages the recipient directly using SendMessage with `type: "message"` and the recipient's name +3. **Sender also messages the lead** with a brief summary: "HANDOFF #N: Sent [details] directly to [recipient]" +4. **Lead logs** the handoff in `progress.md` Handoffs section (audit trail preserved) + +### Key Rule + +The audit trail MUST be maintained. Direct handoffs save time but must still be logged via the lead's workspace updates. diff --git a/docs/report-format.md b/docs/report-format.md index eecb1e3..e7f6310 100644 --- a/docs/report-format.md +++ b/docs/report-format.md @@ -13,7 +13,7 @@ The final report is a persistent artifact generated at completion. It lives in t `.agent-team/{team-name}/report.md` (relative to project root) -This file is generated during Phase 5, step 6. It is the last file written before shutdown. +This file is generated during Phase 5, step 7. It is the last file written before shutdown. ## Template diff --git a/docs/worker-roles.md b/docs/worker-roles.md index dd97bb0..582a9b7 100644 --- a/docs/worker-roles.md +++ b/docs/worker-roles.md @@ -119,6 +119,7 @@ Communication protocol — send structured messages to the lead: - QUESTION: {what I need to know, what I already checked in workspace} Rules: +- At the start of your first task, create a feature branch: `git checkout -b {team-name}/{your-name}`. All your work goes on this branch. If git is not available, skip branching and work directly. - ONLY modify files in your owned area. If you need changes elsewhere, message the lead. - Before starting each new task, re-read workspace files (progress.md, tasks.md, issues.md) to ensure you have current state. This prevents context drift on long-running sessions. - Send STARTING before beginning each task. Send COMPLETED after finishing (include files changed). @@ -302,4 +303,18 @@ Key parameters: ## Subagent Usage Within Teammates -Teammates can spawn subagents (Task tool) for self-contained subtasks that don't need cross-teammate communication. Use them to parallelize within your own scope — e.g., writing tests while implementing, or reading multiple files simultaneously. Do NOT use subagents when the subtask needs input from another teammate or requires back-and-forth iteration. +Teammates can spawn subagents (Task tool) for self-contained subtasks that don't need cross-teammate communication. + +### Standard Usage +Use subagents to parallelize within your own scope — e.g., writing tests while implementing, or reading multiple files simultaneously. Do NOT use subagents when the subtask needs input from another teammate. + +### Nested Task Decomposition (Senior Implementers) +When explicitly authorized by the lead in the spawn prompt, senior implementers may: +- Create sub-tasks using TaskCreate with IDs prefixed by their parent task (e.g., if working on task #3, create sub-tasks described as "#3.1 — [subject]", "#3.2 — [subject]") +- Spawn subagents to work on sub-tasks in parallel +- Report rolled-up results to the lead (the lead sees sub-tasks in TaskList but only interacts at the parent level) + +**Limits:** +- One level of nesting max — sub-subagents cannot create further sub-tasks +- Sub-tasks must be within the teammate's owned file scope +- The teammate is responsible for coordinating their sub-agents (the lead does not manage them) diff --git a/docs/workspace-templates.md b/docs/workspace-templates.md index d64d11e..50de1d0 100644 --- a/docs/workspace-templates.md +++ b/docs/workspace-templates.md @@ -7,6 +7,7 @@ Templates for the 3 workspace tracking files initialized during Phase 3. The lea - [progress.md](#progressmd) — team status, members, phase checklist, decisions, handoffs - [tasks.md](#tasksmd) — task ledger with status tracking - [issues.md](#issuesmd) — issue tracker with severity and impact +- [Additional Workspace Files](#additional-workspace-files) — files created during Phase 3/4 (not template-based) ## progress.md @@ -97,3 +98,31 @@ Cross-teammate information transfers. - **rework**: Completed work must be redone - **deferred**: Logged for post-team follow-up ```` + +## Additional Workspace Files + +These files are created during Phase 3/4 but are not template-based — they are generated from runtime data. + +### file-locks.json + +Created during Phase 3 after spawning teammates. Maps each teammate to their owned files/directories. Used by the PreToolUse(Write|Edit) hook to enforce file ownership. + +```json +{ + "teammate-name": ["src/auth/", "src/middleware/auth.ts"], + "other-teammate": ["src/api/", "tests/api/"] +} +``` + +### events.log + +Created by the SubagentStart/SubagentStop hooks during Phase 4. Each line is a JSON object recording teammate spawn and stop events. Used for post-mortem analysis. + +```json +{"ts":"2026-03-01T00:00:00Z","type":"spawn","agent":"backend-impl","agent_type":"general-purpose"} +{"ts":"2026-03-01T01:00:00Z","type":"stop","agent":"backend-impl"} +``` + +### report.md + +Generated during Phase 5 using the template in [report-format.md](report-format.md). This is the final artifact written before shutdown. diff --git a/hooks/hooks.json b/hooks/hooks.json index 401ff20..052f28a 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,5 +1,5 @@ { - "description": "Agent Team quality gates — prevents premature task completion and nudges idle teammates", + "description": "Agent Team quality gates — prevents premature task completion, nudges idle teammates, enforces file ownership, recovers context after compaction, and tracks teammate lifecycle", "hooks": { "TaskCompleted": [ { @@ -22,6 +22,52 @@ } ] } + ], + "SessionStart": [ + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/recover-context.sh", + "timeout": 10 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-file-ownership.sh", + "timeout": 10 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/track-teammate-lifecycle.sh", + "timeout": 5 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/track-teammate-lifecycle.sh", + "timeout": 5 + } + ] + } ] } } diff --git a/package.json b/package.json index a877ed7..e473aca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-team-plugin", - "version": "1.4.0", + "version": "2.0.0", "description": "Claude Code plugin for orchestrating parallel work via Agent Teams", "license": "MIT", "keywords": ["claude-code", "plugin", "agent-team", "orchestration", "parallel"], diff --git a/scripts/check-file-ownership.sh b/scripts/check-file-ownership.sh new file mode 100755 index 0000000..b059f5f --- /dev/null +++ b/scripts/check-file-ownership.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Hook: PreToolUse (matcher: Write|Edit) +# Enforces file ownership — warns on first violation, blocks on second. +# Exit 0 = allow, Exit 2 = block with feedback. + +# Graceful jq fallback +if ! command -v jq &>/dev/null; then + exit 0 +fi + +INPUT=$(cat) +TEAMMATE=$(echo "$INPUT" | jq -r '.teammate_name // empty') +TEAM=$(echo "$INPUT" | jq -r '.team_name // empty') +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +# Skip if not in team context +if [ -z "$TEAMMATE" ] || [ -z "$TEAM" ] || [ -z "$FILE_PATH" ]; then + exit 0 +fi + +# Normalize FILE_PATH to relative (strip git repo root prefix) +# Claude Code tools may provide absolute paths; ownership checks use relative paths. +GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +if [ -n "$GIT_ROOT" ]; then + FILE_PATH="${FILE_PATH#$GIT_ROOT/}" +fi + +# Always allow workspace file writes +if echo "$FILE_PATH" | grep -qE '(^|/)\.agent-team/'; then + exit 0 +fi + +# Find file-locks.json +LOCKS_FILE=".agent-team/$TEAM/file-locks.json" +if [ ! -f "$LOCKS_FILE" ]; then + # Try -fix suffix (remediation team) + BASE_NAME="${TEAM%-fix}" + if [ "$BASE_NAME" != "$TEAM" ] && [ -f ".agent-team/$BASE_NAME/file-locks.json" ]; then + LOCKS_FILE=".agent-team/$BASE_NAME/file-locks.json" + else + exit 0 # No locks file — graceful degradation + fi +fi + +# Check if teammate owns this file +# file-locks.json: {"teammate-name": ["path/", "path/file.ext"], ...} +OWNED_PATHS=$(jq -r --arg t "$TEAMMATE" '.[$t] // [] | .[]' "$LOCKS_FILE" 2>/dev/null) + +if [ -z "$OWNED_PATHS" ]; then + # Teammate not in file-locks.json — warn but allow + echo "Warning: $TEAMMATE is not listed in file-locks.json. Contact the lead to update file ownership." >&2 + exit 0 +fi + +# Check if file matches any owned path +OWNS_FILE=false +while IFS= read -r owned_path; do + [ -z "$owned_path" ] && continue + # Directory ownership: owned_path ends with / + if [[ "$owned_path" == */ ]]; then + if [[ "$FILE_PATH" == "$owned_path"* ]]; then + OWNS_FILE=true + break + fi + else + # Exact file match + if [ "$FILE_PATH" = "$owned_path" ]; then + OWNS_FILE=true + break + fi + fi +done <<< "$OWNED_PATHS" + +if [ "$OWNS_FILE" = true ]; then + exit 0 +fi + +# --- Violation detected --- +# Warn-then-block: track violations per teammate+file +VIOLATION_DIR="/tmp/agent-team-ownership-violations" +mkdir -p "$VIOLATION_DIR" +chmod 700 "$VIOLATION_DIR" + +# Use md5/shasum for file path hash to avoid path characters in filename +FILE_HASH=$(echo -n "$FILE_PATH" | md5 2>/dev/null || echo -n "$FILE_PATH" | md5sum 2>/dev/null | cut -d' ' -f1 || echo -n "$FILE_PATH" | shasum 2>/dev/null | cut -d' ' -f1) +VIOLATION_FILE="$VIOLATION_DIR/${TEAM}--${TEAMMATE}--${FILE_HASH}" + +if [ -f "$VIOLATION_FILE" ]; then + # Second violation — block + echo "BLOCKED: $TEAMMATE does not own '$FILE_PATH'. This is the second attempt. Message the lead to request ownership reassignment." >&2 + exit 2 +else + # First violation — warn + echo "1" > "$VIOLATION_FILE" + echo "WARNING: File ownership violation — $TEAMMATE does not own '$FILE_PATH'. The owner should handle this file. If you need to modify it, message the lead. Next attempt will be blocked." >&2 + exit 0 +fi diff --git a/scripts/merge-worktrees.sh b/scripts/merge-worktrees.sh new file mode 100755 index 0000000..9275362 --- /dev/null +++ b/scripts/merge-worktrees.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Merges all teammate worktree branches back to the current branch and cleans up. +# Usage: merge-worktrees.sh +# Exit 0 = success (or nothing to merge), Exit 1 = merge conflict (logged to stderr). + +set -euo pipefail + +TEAM_NAME="${1:-}" + +if [ -z "$TEAM_NAME" ]; then + echo "Usage: merge-worktrees.sh " >&2 + exit 1 +fi + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Not a git repo — skipping merge" >&2 + exit 0 +fi + +# Find teammate branches for this team +BRANCHES=$(git branch --list "${TEAM_NAME}/*" 2>/dev/null | sed 's/^[ *+]*//') + +if [ -z "$BRANCHES" ]; then + echo "No branches found for team ${TEAM_NAME}" >&2 + exit 0 +fi + +CONFLICT_BRANCHES="" + +while IFS= read -r branch; do + [ -z "$branch" ] && continue + echo "Merging $branch..." + + # Remove worktree first (if it exists) + WORKTREE_PATH=$(echo "$branch" | sed "s|/|--|g") + if [ -d ".claude/worktrees/$WORKTREE_PATH" ]; then + git worktree remove ".claude/worktrees/$WORKTREE_PATH" --force 2>/dev/null || true + fi + + if git merge --no-ff "$branch" -m "Merge teammate branch $branch" 2>/dev/null; then + git branch -d "$branch" 2>/dev/null || true + echo " Merged successfully" + else + git merge --abort 2>/dev/null || true + CONFLICT_BRANCHES="$CONFLICT_BRANCHES $branch" + echo " CONFLICT — merge aborted" >&2 + fi +done <<< "$BRANCHES" + +if [ -n "$CONFLICT_BRANCHES" ]; then + echo "Merge conflicts on branches:$CONFLICT_BRANCHES" >&2 + echo "Resolve manually or assign to an implementer." >&2 + exit 1 +fi + +exit 0 diff --git a/scripts/recover-context.sh b/scripts/recover-context.sh new file mode 100755 index 0000000..2f96f1a --- /dev/null +++ b/scripts/recover-context.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Hook: SessionStart (compact matcher) +# After context compaction, outputs active workspace state to help the lead recover. +# Exit 0 always (non-blocking). Summary output goes to stdout (injected into context). + +# Graceful jq fallback +if ! command -v jq &>/dev/null; then + exit 0 +fi + +INPUT=$(cat) +CWD=$(echo "$INPUT" | jq -r '.cwd // empty') + +# Use cwd from input, fall back to current directory +SEARCH_DIR="${CWD:-.}" + +# Find active workspaces (status != done) +FOUND_ACTIVE=false +for progress_file in "$SEARCH_DIR"/.agent-team/*/progress.md; do + [ -f "$progress_file" ] || continue + + # Check if workspace is active (not done) + STATUS=$(sed -n 's/^\*\*Status\*\*: *//p' "$progress_file" | tr -d ' ') + if [ "$STATUS" = "done" ]; then + continue + fi + + TEAM_DIR=$(dirname "$progress_file") + TEAM_NAME=$(basename "$TEAM_DIR") + FOUND_ACTIVE=true + + echo "=== CONTEXT RECOVERY: Active team workspace found ===" + echo "" + echo "Team: $TEAM_NAME" + echo "Workspace: .agent-team/$TEAM_NAME/" + echo "Status: $STATUS" + echo "" + echo "Recovery action: Read these files to restore your awareness:" + echo " 1. .agent-team/$TEAM_NAME/progress.md (team state, decisions, handoffs)" + echo " 2. .agent-team/$TEAM_NAME/tasks.md (task ledger with statuses)" + echo " 3. .agent-team/$TEAM_NAME/issues.md (open issues)" + echo "" + echo "Then read ~/.claude/teams/$TEAM_NAME/config.json for live team members." + echo "Then call TaskList for live task state." + echo "=== END CONTEXT RECOVERY ===" +done + +exit 0 diff --git a/scripts/setup-worktree.sh b/scripts/setup-worktree.sh new file mode 100755 index 0000000..3aed928 --- /dev/null +++ b/scripts/setup-worktree.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Creates a git worktree for an isolated teammate workspace. +# Usage: setup-worktree.sh +# Outputs the worktree path to stdout on success. +# Exit 0 = success, Exit 1 = error. + +set -euo pipefail + +TEAM_NAME="${1:-}" +TEAMMATE_NAME="${2:-}" + +if [ -z "$TEAM_NAME" ] || [ -z "$TEAMMATE_NAME" ]; then + echo "Usage: setup-worktree.sh " >&2 + exit 1 +fi + +# Must be in a git repo +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Error: not inside a git repository" >&2 + exit 1 +fi + +WORKTREE_DIR=".claude/worktrees/${TEAM_NAME}--${TEAMMATE_NAME}" +BRANCH_NAME="${TEAM_NAME}/${TEAMMATE_NAME}" + +# Create worktree with a new branch +git worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME" HEAD 2>/dev/null + +echo "$WORKTREE_DIR" diff --git a/scripts/track-teammate-lifecycle.sh b/scripts/track-teammate-lifecycle.sh new file mode 100755 index 0000000..4e74930 --- /dev/null +++ b/scripts/track-teammate-lifecycle.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Hook: SubagentStart / SubagentStop +# Appends lifecycle events to .agent-team/{team}/events.log. +# Non-blocking — always exits 0. + +# Graceful jq fallback +if ! command -v jq &>/dev/null; then + exit 0 +fi + +INPUT=$(cat) +EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty') +TEAMMATE=$(echo "$INPUT" | jq -r '.teammate_name // empty') +TEAM=$(echo "$INPUT" | jq -r '.team_name // empty') +AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // empty') + +# Skip if we can't identify the team +if [ -z "$TEAM" ]; then + exit 0 +fi + +# Find workspace directory +WORKSPACE_DIR=".agent-team/$TEAM" +if [ ! -d "$WORKSPACE_DIR" ]; then + BASE_NAME="${TEAM%-fix}" + if [ "$BASE_NAME" != "$TEAM" ] && [ -d ".agent-team/$BASE_NAME" ]; then + WORKSPACE_DIR=".agent-team/$BASE_NAME" + else + exit 0 # No workspace — nothing to log + fi +fi + +EVENTS_LOG="$WORKSPACE_DIR/events.log" +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ") + +case "$EVENT" in + SubagentStart) + echo "{\"ts\":\"$TIMESTAMP\",\"type\":\"spawn\",\"agent\":\"$TEAMMATE\",\"agent_type\":\"$AGENT_TYPE\"}" >> "$EVENTS_LOG" + ;; + SubagentStop) + echo "{\"ts\":\"$TIMESTAMP\",\"type\":\"stop\",\"agent\":\"$TEAMMATE\"}" >> "$EVENTS_LOG" + ;; +esac + +exit 0 diff --git a/scripts/verify-task-complete.sh b/scripts/verify-task-complete.sh index 2f669a5..4ff153b 100755 --- a/scripts/verify-task-complete.sh +++ b/scripts/verify-task-complete.sh @@ -11,6 +11,8 @@ fi INPUT=$(cat) TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // empty') TEAM_NAME=$(echo "$INPUT" | jq -r '.team_name // empty') +TASK_ID=$(echo "$INPUT" | jq -r '.task_id // empty') +TEAMMATE_NAME=$(echo "$INPUT" | jq -r '.teammate_name // empty') # Skip validation if no task subject if [ -z "$TASK_SUBJECT" ]; then @@ -54,13 +56,25 @@ fi # Implementation keywords take precedence over skip keywords. # This prevents "Write tests for audit module" from being treated as workspace-only. if echo "$TASK_SUBJECT" | grep -qiE 'implement|create|add|build|write|refactor|fix|migrate'; then - # Check git for changes (if in a git repo). - # Trade-off: this checks ALL repo changes (staged + unstaged + untracked), - # not just changes made by this specific teammate. Unrelated dirty files - # will cause this to always pass; conversely, a teammate who only modifies - # workspace files may be falsely blocked. Accepted as good-enough heuristic. if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') + CHANGES="0" + # Try scoped check first: if we know the teammate and their owned files, only check those + if [ -n "$TEAMMATE_NAME" ] && [ -n "$WORKSPACE_DIR" ] && [ -f "$WORKSPACE_DIR/file-locks.json" ]; then + OWNED_PATHS=$(jq -r --arg t "$TEAMMATE_NAME" '.[$t] // [] | .[]' "$WORKSPACE_DIR/file-locks.json" 2>/dev/null) + if [ -n "$OWNED_PATHS" ]; then + while IFS= read -r owned_path; do + [ -z "$owned_path" ] && continue + PATH_CHANGES=$(git status --porcelain -- "$owned_path" 2>/dev/null | wc -l | tr -d ' ') + CHANGES=$((CHANGES + PATH_CHANGES)) + done <<< "$OWNED_PATHS" + else + # Teammate not in file-locks — fall back to repo-wide + CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') + fi + else + # No scoping info — fall back to repo-wide (original behavior) + CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') + fi if [ "$CHANGES" = "0" ]; then echo "Implementation task marked complete but no file changes detected. Verify your work was saved, then mark complete again." >&2 exit 2 diff --git a/skills/agent-team/SKILL.md b/skills/agent-team/SKILL.md index 8a794cd..7c2ee40 100644 --- a/skills/agent-team/SKILL.md +++ b/skills/agent-team/SKILL.md @@ -25,12 +25,15 @@ Agent Teams require the experimental feature flag. Before proceeding, verify it ## Hooks -This plugin registers two hooks at the plugin level via `hooks/hooks.json` (not in skill frontmatter). They enforce team discipline automatically: +This plugin registers hooks at the plugin level via `hooks/hooks.json`. They enforce team discipline automatically: -- **TaskCompleted** (`scripts/verify-task-complete.sh`): Blocks premature task completion — checks that workspace files exist and that implementation tasks have actual file changes. Requires `jq` (gracefully skips if missing); uses `git` for change detection (skips if not a repo). +- **TaskCompleted** (`scripts/verify-task-complete.sh`): Blocks premature task completion — checks workspace files exist and implementation tasks have actual file changes. Uses `teammate_name` and `file-locks.json` to scope git checks to the teammate's owned files when available. Requires `jq`. - **TeammateIdle** (`scripts/check-teammate-idle.sh`): Nudges idle teammates that still have in-progress tasks. Includes loop protection (allows idle after 3 blocked attempts). Requires `jq`. +- **SessionStart(compact)** (`scripts/recover-context.sh`): After context compaction, automatically outputs active workspace paths and recovery instructions. Non-blocking. +- **PreToolUse(Write|Edit)** (`scripts/check-file-ownership.sh`): Enforces file ownership via `file-locks.json`. Warn-then-block: first violation warns, second blocks. Workspace files (`.agent-team/`) always allowed. Requires `jq`. +- **SubagentStart / SubagentStop** (`scripts/track-teammate-lifecycle.sh`): Logs teammate spawn and stop events to `.agent-team/{team}/events.log`. Non-blocking. -Both hooks exit 0 (allow) if their dependencies are missing — they degrade gracefully. Hook paths use `${CLAUDE_PLUGIN_ROOT}` so they resolve correctly regardless of install location. +All hooks exit 0 (allow) if their dependencies are missing — they degrade gracefully. Hook paths use `${CLAUDE_PLUGIN_ROOT}`. ## Phase 1: Analyze and Decompose @@ -78,6 +81,9 @@ Every phase has an owner (omit for pure review tasks): - Testing: [role] (required for complex plans) - Finalization: [role] +Isolation: shared (default) | worktree + (if worktree) Each implementer gets a git worktree with a dedicated branch. Zero conflict risk. + Workspace: .agent-team/[team-name]/ Estimated teammates: N ``` @@ -106,6 +112,28 @@ Wait for user confirmation before proceeding. - `.agent-team/{team-name}/tasks.md` — task ledger with status tracking - `.agent-team/{team-name}/issues.md` — issue tracker with severity and impact + #### file-locks.json + + ```json + { + "{teammate-name}": ["{owned-directory}/", "{owned-file}"], + "{teammate-name}": ["{owned-directory}/"] + } + ``` + + Populated from the Phase 2 plan's file ownership mapping. Used by the PreToolUse hook to enforce ownership. + + #### events.log + + Initially empty. Append-only, one JSON line per event. The SubagentStart/Stop hooks write to this file automatically. The lead also appends events during Phase 4 coordination. + + Event types: `spawn`, `stop`, `task_start`, `task_complete`, `blocked`, `handoff`, `decision`, `replan`. + + Format: + ```json + {"ts":"2026-02-27T10:30:00Z","type":"spawn","agent":"backend-impl","agent_type":"general-purpose"} + ``` + The workspace is your persistent memory AND the team's shared state. It MUST exist before any tasks are created. If a `.gitignore` exists and doesn't already exclude `.agent-team/`, add it. Workspace files are coordination artifacts, not project deliverables. @@ -139,9 +167,17 @@ Wait for user confirmation before proceeding. 7. After completing a task: mark complete via TaskUpdate, check TaskList, self-claim next available 8. Use subagents (Task tool) for focused subtasks that don't need teammate communication 9. Write output artifacts to the workspace directory + - **Branch instruction** (implementers only): "Create branch `{team-name}/{your-name}` before starting work. If git is unavailable, skip." + - **Nested decomposition** (optional): For large tasks, tell senior implementers: "You may create sub-tasks and spawn subagents for independent portions of your work. Report rolled-up results to me. One level of nesting max." **Update workspace**: record each teammate in `progress.md` Team Members table +5b. **Create worktrees** (if `isolation: worktree`): + - For each implementer, run `scripts/setup-worktree.sh {team-name} {teammate-name}` + - Include the worktree path in the implementer's spawn prompt as their working directory + - If worktree creation fails for any teammate, fall back to shared mode for that teammate and log a warning in `issues.md` + - File ownership hook (PreToolUse) is redundant in worktree mode but remains active as a safety net + 6. **Team size gate** — explicitly count before spawning: "I am spawning N teammates: [list names]." - **Default max: 4** for mixed teams (implementers + reviewers/challengers) - **Up to 6** if the additional teammates beyond 4 are **read-only** (researchers, reviewers using `subagent_type: "Explore"`) — read-only agents have zero file conflict risk and minimal coordination cost @@ -195,6 +231,12 @@ When multiple events arrive close together, batch them into a single edit per fi | Issue resolved | issues.md | Status -> RESOLVED/MITIGATED, update counts | | Teammate status change | progress.md | Update Team Members table | | All work done | progress.md | Status -> `done` | +| Teammate spawned | events.log | Append spawn event (also auto-logged by SubagentStart hook) | +| Task started | events.log | Append task_start event | +| Task completed | events.log | Append task_complete event | +| Blocked event | events.log | Append blocked event | +| Handoff occurs | events.log | Append handoff event | +| Decision made | events.log | Append decision event | ### Communication Protocol @@ -224,6 +266,8 @@ When receiving structured messages: When a teammate spawned with `mode: "plan"` finishes planning, they send a `plan_approval_request` message to the lead. You must respond via SendMessage with `type: "plan_approval_response"`, the teammate as `recipient`, the `request_id` from their request, and `approve: true` or `approve: false`. If rejecting, include `content` with specific feedback so the teammate can revise their plan. The teammate cannot proceed with implementation until the plan is approved. +For high-frequency handoffs between specific teammates, you may authorize direct communication — see the Direct Handoff pattern in [coordination-patterns.md](../../docs/coordination-patterns.md). The audit trail must still be maintained in `progress.md`. + ### Coordination Patterns For detailed patterns on these scenarios, see [coordination-patterns.md](../../docs/coordination-patterns.md): @@ -246,6 +290,7 @@ For detailed patterns on these scenarios, see [coordination-patterns.md](../../d - **Adversarial review rounds** — multi-round cross-review for high-stakes changes - **Quality gate** — final validation pass before Phase 5 synthesis - **Auto-block on repeated failures** — auto-escalation after 3 blocked attempts +- **Direct handoff** — authorized peer-to-peer messaging with audit trail **Periodic scan**: on every context recovery, check `issues.md` for OPEN items and address them before resuming normal coordination. @@ -277,45 +322,52 @@ The phase checklist is embedded in your `progress.md` — check it during worksp - Log the failure in `issues.md` as **high** severity - Only read-only teammates (reviewers, researchers, challengers, testers) are exempt — they have no files to commit -4. **Check integration** — do the pieces fit together? If issues found, assign fixes before wrapping up +4. **Merge branches** (if auto-branching or worktree isolation was used): + - If worktree isolation: run `scripts/merge-worktrees.sh {team-name}` to merge all teammate branches and clean up worktrees + - If auto-branching only: for each branch, `git merge --no-ff {team-name}/{teammate-name}` + - If merge conflicts: log in `issues.md`, assign the relevant implementer to resolve + - If neither branching nor worktrees were used, skip this step + +5. **Check integration** — do the pieces fit together? If issues found, assign fixes before wrapping up **Self-check**: "Did I verify that the pieces integrate? If issues were found, have I assigned fixes before proceeding?" If no, STOP — do not generate the report until integration is confirmed. -5. **Update workspace**: set `progress.md` status to `completing`, update `tasks.md` with final states and teammate notes. See Workspace Update Protocol in Phase 4 for event-to-file mappings. +6. **Update workspace**: set `progress.md` status to `completing`, update `tasks.md` with final states and teammate notes. See Workspace Update Protocol in Phase 4 for event-to-file mappings. -6. **Generate final report** (MANDATORY — do not skip): +7. **Generate final report** (MANDATORY — do not skip): - Read all workspace files for full history - Read TaskList for final task states - Write `.agent-team/{team-name}/report.md` using the format in [report-format.md](../../docs/report-format.md) - **Self-check**: "Does `.agent-team/{team-name}/report.md` exist and contain the executive summary?" If no, generate it now -7. **Remediation gate** — review `issues.md` for OPEN issues: - - If **0 OPEN issues**: skip to step 8 - - If **OPEN issues exist** and `progress.md` remediation cycle is already `1`: do NOT spawn another team. Include unresolved issues in the user report (step 8): +8. **Remediation gate** — review `issues.md` for OPEN issues: + - If **0 OPEN issues**: skip to step 9 + - If **OPEN issues exist** and `progress.md` remediation cycle is already `1`: do NOT spawn another team. Include unresolved issues in the user report (step 9): > **Unresolved issues (require manual follow-up):** > - Issue #N (severity): description > See `.agent-team/{team-name}/issues.md` for full details. - If **OPEN issues exist** and remediation cycle is `0`: present issues to the user and propose a remediation team. Follow the full protocol in [coordination-patterns.md](../../docs/coordination-patterns.md#remediation-gate). -8. **Report to user**: +9. **Report to user**: - Summary of all work completed - Files modified by each teammate - **Issues summary**: list any OPEN or MITIGATED issues from `issues.md` with their impact - Any open concerns or follow-up items - **Workspace path**: tell the user where the workspace is (`.agent-team/{team-name}/`) -9. **Shutdown sequence** (parallel — do NOT wait for each one sequentially): - ``` - Send ALL shutdown_request messages in a single turn (parallel SendMessage calls) - Wait for all approval responses - If a teammate rejects: check their reason, resolve, then re-request - ``` - **Update workspace**: set `progress.md` status to `done`, record completion time +10. **Shutdown sequence** (parallel — do NOT wait for each one sequentially): + ``` + Send ALL shutdown_request messages in a single turn (parallel SendMessage calls) + Wait for all approval responses + If a teammate rejects: check their reason, resolve, then re-request + ``` + **Update workspace**: set `progress.md` status to `done`, record completion time -10. **Cleanup**: +11. **Cleanup**: - **Only call TeamDelete after ALL teammates have confirmed shutdown.** TeamDelete may fail if teammates are still active — always wait for all shutdown confirmations first. - TeamDelete to remove ephemeral team resources (`~/.claude/teams/{team-name}/`). The workspace at `.agent-team/{team-name}/` is NOT deleted — it is the permanent record - Clean up idle hook counters: `rm -f /tmp/agent-team-idle-counters/{team-name}--* 2>/dev/null || true` + - Clean up ownership violation tracking: `rm -rf /tmp/agent-team-ownership-violations 2>/dev/null || true` ## Reference diff --git a/tests/hooks/test-check-file-ownership.sh b/tests/hooks/test-check-file-ownership.sh new file mode 100755 index 0000000..3fff2ef --- /dev/null +++ b/tests/hooks/test-check-file-ownership.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Tests for scripts/check-file-ownership.sh (PreToolUse file ownership hook) + +source "$(dirname "$0")/../lib/test-helpers.sh" + +HOOK="$PROJECT_ROOT/scripts/check-file-ownership.sh" + +echo "PreToolUse file ownership hook tests" +echo "=====================================" + +if ! command -v jq &>/dev/null; then + printf " ${YELLOW}SKIP${RESET} all — jq not installed\n" + exit 0 +fi + +# Helper: create file-locks.json +create_file_locks() { + local workspace_dir="$1" + cat > "$workspace_dir/file-locks.json" <<'EOF' +{ + "backend-impl": ["src/auth/", "src/middleware/auth.ts"], + "frontend-impl": ["src/components/", "src/pages/"] +} +EOF +} + +# --- Test 1: No file-locks.json — allow (graceful degradation) --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +run_hook "$HOOK" '{"tool_name":"Write","tool_input":{"file_path":"src/auth/login.ts"},"teammate_name":"backend-impl","team_name":"test"}' +assert_exit_code 0 "$HOOK_EXIT" "1: No file-locks.json allows (graceful degradation)" +cleanup_temp_dir + +# --- Test 2: Teammate writes to owned file — allow --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +run_hook "$HOOK" '{"tool_name":"Write","tool_input":{"file_path":"src/auth/login.ts"},"teammate_name":"backend-impl","team_name":"test"}' +assert_exit_code 0 "$HOOK_EXIT" "2: Write to owned file allows" +cleanup_temp_dir + +# --- Test 3: Teammate writes to unowned file — first violation warns --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +# Clear any existing violation counters +rm -rf /tmp/agent-team-ownership-violations 2>/dev/null +run_hook "$HOOK" '{"tool_name":"Write","tool_input":{"file_path":"src/components/Button.tsx"},"teammate_name":"backend-impl","team_name":"test"}' +assert_exit_code 0 "$HOOK_EXIT" "3: First violation warns (exit 0)" +assert_stderr_contains "ownership" "$HOOK_STDERR" "3: Warning message mentions ownership" +cleanup_temp_dir + +# --- Test 4: Second violation on same file — blocks --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +rm -rf /tmp/agent-team-ownership-violations 2>/dev/null +# First violation (warn) +run_hook "$HOOK" '{"tool_name":"Write","tool_input":{"file_path":"src/components/Button.tsx"},"teammate_name":"backend-impl","team_name":"test"}' +# Second violation (block) +run_hook "$HOOK" '{"tool_name":"Write","tool_input":{"file_path":"src/components/Button.tsx"},"teammate_name":"backend-impl","team_name":"test"}' +assert_exit_code 2 "$HOOK_EXIT" "4: Second violation blocks (exit 2)" +cleanup_temp_dir + +# --- Test 5: Workspace files always allowed --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +run_hook "$HOOK" '{"tool_name":"Write","tool_input":{"file_path":".agent-team/test/tasks.md"},"teammate_name":"backend-impl","team_name":"test"}' +assert_exit_code 0 "$HOOK_EXIT" "5: Workspace file write always allowed" +cleanup_temp_dir + +# --- Test 6: No teammate_name — allow (not a team context) --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +run_hook "$HOOK" '{"tool_name":"Write","tool_input":{"file_path":"src/foo.ts"}}' +assert_exit_code 0 "$HOOK_EXIT" "6: No teammate_name allows" +cleanup_temp_dir + +# --- Test 7: Directory ownership matches file inside directory --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +run_hook "$HOOK" '{"tool_name":"Edit","tool_input":{"file_path":"src/auth/middleware/validate.ts"},"teammate_name":"backend-impl","team_name":"test"}' +assert_exit_code 0 "$HOOK_EXIT" "7: File inside owned directory is allowed" +cleanup_temp_dir + +# --- Test 8: Absolute path to workspace file is exempted --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +ABS_WORKSPACE_PATH="$TEST_TEMP_DIR/.agent-team/test/progress.md" +run_hook "$HOOK" "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$ABS_WORKSPACE_PATH\"},\"teammate_name\":\"backend-impl\",\"team_name\":\"test\"}" +assert_exit_code 0 "$HOOK_EXIT" "8: Absolute path to workspace file is exempted" +cleanup_temp_dir + +# --- Test 9: Absolute path to owned file matches ownership --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +ABS_OWNED_PATH="$TEST_TEMP_DIR/src/auth/login.ts" +run_hook "$HOOK" "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$ABS_OWNED_PATH\"},\"teammate_name\":\"backend-impl\",\"team_name\":\"test\"}" +assert_exit_code 0 "$HOOK_EXIT" "9: Absolute path to owned file matches ownership" +cleanup_temp_dir + +# --- Test 10: Absolute path to unowned file still triggers violation --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo +setup_mock_workspace "test" +create_file_locks "$WORKSPACE_DIR" +rm -rf /tmp/agent-team-ownership-violations 2>/dev/null +ABS_UNOWNED_PATH="$TEST_TEMP_DIR/src/components/Button.tsx" +run_hook "$HOOK" "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$ABS_UNOWNED_PATH\"},\"teammate_name\":\"backend-impl\",\"team_name\":\"test\"}" +assert_exit_code 0 "$HOOK_EXIT" "10: Absolute path to unowned file — first violation warns" +assert_stderr_contains "ownership" "$HOOK_STDERR" "10: Warning message mentions ownership" +cleanup_temp_dir + +# Cleanup violation counters +rm -rf /tmp/agent-team-ownership-violations 2>/dev/null + +print_summary +exit "$TESTS_FAILED" diff --git a/tests/hooks/test-merge-worktrees.sh b/tests/hooks/test-merge-worktrees.sh new file mode 100755 index 0000000..639924c --- /dev/null +++ b/tests/hooks/test-merge-worktrees.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Tests for scripts/merge-worktrees.sh + +source "$(dirname "$0")/../lib/test-helpers.sh" + +SCRIPT="$PROJECT_ROOT/scripts/merge-worktrees.sh" + +echo "Worktree merge script tests" +echo "============================" + +# --- Test 1: Merges worktree branch back --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo "clean" +# Create a worktree and make a change in it +git worktree add .claude/worktrees/test--impl -b test/impl HEAD 2>/dev/null +(cd .claude/worktrees/test--impl && echo "new file" > feature.txt && git add feature.txt && git commit -q -m "add feature") +RESULT=$(bash "$SCRIPT" "test" 2>&1) +SCRIPT_EXIT=$? +assert_exit_code 0 "$SCRIPT_EXIT" "1: Merge succeeds" +assert_true "1: Feature file exists on main branch" '[ -f "feature.txt" ]' +cleanup_temp_dir + +# --- Test 2: No worktrees to merge — exits 0 --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo "clean" +RESULT=$(bash "$SCRIPT" "nonexistent" 2>&1) +SCRIPT_EXIT=$? +assert_exit_code 0 "$SCRIPT_EXIT" "2: No worktrees exits 0" +cleanup_temp_dir + +# --- Test 3: Missing argument — exits 1 with usage message --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +STDERR_OUTPUT=$(bash "$SCRIPT" 2>&1 1>/dev/null) +SCRIPT_EXIT=$? +assert_exit_code 1 "$SCRIPT_EXIT" "3: Missing argument exits 1" +assert_stderr_contains "Usage" "$STDERR_OUTPUT" "3: Missing argument prints usage to stderr" +cleanup_temp_dir + +# --- Test 4: Merge conflict — exits 1 with conflict message --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo "clean" +# Create a teammate branch that modifies committed-file.txt +git worktree add .claude/worktrees/conflict--worker -b conflict/worker HEAD 2>/dev/null +(cd .claude/worktrees/conflict--worker && echo "teammate change" > committed-file.txt && git add committed-file.txt && git commit -q -m "teammate edits file") +# Also modify committed-file.txt on the main branch to create a conflict +echo "main branch change" > committed-file.txt +git add committed-file.txt +git commit -q -m "main edits same file" +# Now merging conflict/worker should fail with a conflict +RESULT=$(bash "$SCRIPT" "conflict" 2>&1) +SCRIPT_EXIT=$? +assert_exit_code 1 "$SCRIPT_EXIT" "4: Merge conflict exits 1" +assert_stderr_contains "CONFLICT" "$RESULT" "4: Merge conflict prints CONFLICT to stderr" +assert_stderr_contains "conflict/worker" "$RESULT" "4: Merge conflict names the conflicting branch" +cleanup_temp_dir + +print_summary +exit "$TESTS_FAILED" diff --git a/tests/hooks/test-recover-context.sh b/tests/hooks/test-recover-context.sh new file mode 100755 index 0000000..077dd1a --- /dev/null +++ b/tests/hooks/test-recover-context.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Tests for scripts/recover-context.sh (SessionStart compact hook) + +source "$(dirname "$0")/../lib/test-helpers.sh" + +HOOK="$PROJECT_ROOT/scripts/recover-context.sh" + +echo "SessionStart(compact) hook tests" +echo "=================================" + +if ! command -v jq &>/dev/null; then + printf " ${YELLOW}SKIP${RESET} all — jq not installed\n" + exit 0 +fi + +# --- Test 1: No workspace directory — outputs nothing, exits 0 --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +HOOK_STDOUT=$(echo '{"hook_event_name":"SessionStart","matcher":"compact","cwd":"'"$TEST_TEMP_DIR"'"}' | bash "$HOOK" 2>/dev/null) +HOOK_EXIT=$? +assert_exit_code 0 "$HOOK_EXIT" "1: No workspace exits 0" +assert_true "1: No output when no workspace" '[ -z "$HOOK_STDOUT" ]' +cleanup_temp_dir + +# --- Test 2: Active workspace — outputs summary --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "my-team" +# Status is already active from setup_mock_workspace +HOOK_STDOUT=$(echo '{"hook_event_name":"SessionStart","matcher":"compact","cwd":"'"$TEST_TEMP_DIR"'"}' | bash "$HOOK" 2>/dev/null) +HOOK_EXIT=$? +assert_exit_code 0 "$HOOK_EXIT" "2: Active workspace exits 0" +assert_true "2: Output contains workspace path" 'echo "$HOOK_STDOUT" | grep -q ".agent-team/my-team"' +cleanup_temp_dir + +# --- Test 3: Done workspace — no output (not active) --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "done-team" +sed -i.bak 's/\*\*Status\*\*: active/**Status**: done/' "$WORKSPACE_DIR/progress.md" 2>/dev/null || \ + sed -i '' 's/\*\*Status\*\*: active/**Status**: done/' "$WORKSPACE_DIR/progress.md" +HOOK_STDOUT=$(echo '{"hook_event_name":"SessionStart","matcher":"compact","cwd":"'"$TEST_TEMP_DIR"'"}' | bash "$HOOK" 2>/dev/null) +HOOK_EXIT=$? +assert_exit_code 0 "$HOOK_EXIT" "3: Done workspace exits 0" +assert_true "3: No output for done workspace" '[ -z "$HOOK_STDOUT" ]' +cleanup_temp_dir + +# --- Test 4: Graceful degradation without jq --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +ORIG_PATH="$PATH" +SHADOW_BIN="$TEST_TEMP_DIR/shadow-bin" +mkdir -p "$SHADOW_BIN" +for bin_dir in /usr/bin /bin; do + if [ -d "$bin_dir" ]; then + for exe in "$bin_dir"/*; do + base=$(basename "$exe") + [ "$base" = "jq" ] && continue + [ ! -e "$SHADOW_BIN/$base" ] && ln -sf "$exe" "$SHADOW_BIN/$base" 2>/dev/null || true + done + fi +done +HOOK_STDOUT=$(echo '{}' | PATH="$SHADOW_BIN" bash "$HOOK" 2>/dev/null) +HOOK_EXIT=$? +PATH="$ORIG_PATH" +assert_exit_code 0 "$HOOK_EXIT" "4: Graceful degradation without jq" +cleanup_temp_dir + +print_summary +exit "$TESTS_FAILED" diff --git a/tests/hooks/test-setup-worktree.sh b/tests/hooks/test-setup-worktree.sh new file mode 100755 index 0000000..89d1222 --- /dev/null +++ b/tests/hooks/test-setup-worktree.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Tests for scripts/setup-worktree.sh + +source "$(dirname "$0")/../lib/test-helpers.sh" + +SCRIPT="$PROJECT_ROOT/scripts/setup-worktree.sh" + +echo "Worktree setup script tests" +echo "============================" + +# --- Test 1: Creates worktree in git repo --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo "clean" +RESULT=$(bash "$SCRIPT" "test-team" "backend-impl" 2>/dev/null) +SCRIPT_EXIT=$? +assert_exit_code 0 "$SCRIPT_EXIT" "1: Creates worktree (exit code)" +assert_true "1: Worktree directory exists" '[ -d ".claude/worktrees/test-team--backend-impl" ]' +assert_true "1: Output contains worktree path" 'echo "$RESULT" | grep -q "worktrees/test-team--backend-impl"' +cleanup_temp_dir + +# --- Test 2: Not a git repo — exits with error --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +RESULT=$(bash "$SCRIPT" "test-team" "backend-impl" 2>/dev/null) +SCRIPT_EXIT=$? +assert_exit_code 1 "$SCRIPT_EXIT" "2: Not a git repo exits 1" +cleanup_temp_dir + +# --- Test 3: Missing arguments — exits with error --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_git_repo "clean" +RESULT=$(bash "$SCRIPT" 2>/dev/null) +SCRIPT_EXIT=$? +assert_exit_code 1 "$SCRIPT_EXIT" "3: Missing arguments exits 1" +cleanup_temp_dir + +print_summary +exit "$TESTS_FAILED" diff --git a/tests/hooks/test-track-teammate-lifecycle.sh b/tests/hooks/test-track-teammate-lifecycle.sh new file mode 100755 index 0000000..f0a8faf --- /dev/null +++ b/tests/hooks/test-track-teammate-lifecycle.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Tests for scripts/track-teammate-lifecycle.sh + +source "$(dirname "$0")/../lib/test-helpers.sh" + +HOOK="$PROJECT_ROOT/scripts/track-teammate-lifecycle.sh" + +echo "SubagentStart/Stop lifecycle hook tests" +echo "========================================" + +if ! command -v jq &>/dev/null; then + printf " ${YELLOW}SKIP${RESET} all — jq not installed\n" + exit 0 +fi + +# --- Test 1: SubagentStart appends to events.log --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +HOOK_STDERR=$(echo '{"hook_event_name":"SubagentStart","teammate_name":"backend-impl","team_name":"test","agent_type":"general-purpose"}' | bash "$HOOK" 2>&1 1>/dev/null) +HOOK_EXIT=$? +assert_exit_code 0 "$HOOK_EXIT" "1: SubagentStart exits 0" +assert_true "1: events.log created" '[ -f ".agent-team/test/events.log" ]' +assert_true "1: events.log contains spawn entry" 'grep -q "spawn" ".agent-team/test/events.log"' +cleanup_temp_dir + +# --- Test 2: SubagentStop appends to events.log --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +echo '{"ts":"2026-01-01T00:00:00Z","type":"spawn","agent":"backend-impl"}' > ".agent-team/test/events.log" +HOOK_STDERR=$(echo '{"hook_event_name":"SubagentStop","teammate_name":"backend-impl","team_name":"test"}' | bash "$HOOK" 2>&1 1>/dev/null) +HOOK_EXIT=$? +assert_exit_code 0 "$HOOK_EXIT" "2: SubagentStop exits 0" +assert_true "2: events.log has 2 entries" '[ "$(wc -l < .agent-team/test/events.log | tr -d " ")" -ge 2 ]' +assert_true "2: events.log contains stop entry" 'grep -q "stop" ".agent-team/test/events.log"' +cleanup_temp_dir + +# --- Test 3: No team_name — exits 0 silently --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +HOOK_STDERR=$(echo '{"hook_event_name":"SubagentStart","teammate_name":"test"}' | bash "$HOOK" 2>&1 1>/dev/null) +HOOK_EXIT=$? +assert_exit_code 0 "$HOOK_EXIT" "3: No team_name exits 0" +cleanup_temp_dir + +# --- Test 4: No workspace — exits 0 silently --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +HOOK_STDERR=$(echo '{"hook_event_name":"SubagentStart","teammate_name":"impl","team_name":"nonexistent"}' | bash "$HOOK" 2>&1 1>/dev/null) +HOOK_EXIT=$? +assert_exit_code 0 "$HOOK_EXIT" "4: No workspace exits 0" +cleanup_temp_dir + +print_summary +exit "$TESTS_FAILED" diff --git a/tests/hooks/test-verify-task-complete.sh b/tests/hooks/test-verify-task-complete.sh index 9643af7..2276796 100755 --- a/tests/hooks/test-verify-task-complete.sh +++ b/tests/hooks/test-verify-task-complete.sh @@ -155,5 +155,22 @@ run_hook "$HOOK" '{"task_subject":"Fix README issues","team_name":"my-project-fi assert_exit_code 0 "$HOOK_EXIT" "12: Remediation team (-fix suffix) finds original workspace" cleanup_temp_dir +# --- Test 13: teammate_name scopes git check to owned files --- +setup_temp_dir +cd "$TEST_TEMP_DIR" +setup_mock_workspace "test" +# Create file-locks.json +cat > "$WORKSPACE_DIR/file-locks.json" <<'LOCKS' +{"backend-impl": ["src/auth/"]} +LOCKS +setup_mock_git_repo "dirty" +# Add a dirty file inside the owned path so scoped check finds changes +mkdir -p src/auth +echo "change" > src/auth/login.ts +run_hook "$HOOK" '{"task_subject":"Implement auth","team_name":"test","task_id":"task-001","teammate_name":"backend-impl"}' +# Should pass because there are dirty files in the owned path (src/auth/) +assert_exit_code 0 "$HOOK_EXIT" "13: Enhanced hook reads task_id and teammate_name" +cleanup_temp_dir + print_summary exit "$TESTS_FAILED"