beads-orchestration 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/SKILL.md +263 -0
  4. package/bootstrap.py +928 -0
  5. package/package.json +37 -0
  6. package/scripts/cli.js +64 -0
  7. package/scripts/postinstall.js +71 -0
  8. package/skills/create-beads-orchestration/SKILL.md +263 -0
  9. package/skills/subagents-discipline/SKILL.md +158 -0
  10. package/templates/CLAUDE.md +326 -0
  11. package/templates/agents/architect.md +121 -0
  12. package/templates/agents/code-reviewer.md +248 -0
  13. package/templates/agents/detective.md +101 -0
  14. package/templates/agents/discovery.md +492 -0
  15. package/templates/agents/merge-supervisor.md +119 -0
  16. package/templates/agents/scout.md +100 -0
  17. package/templates/agents/scribe.md +96 -0
  18. package/templates/beads-workflow-injection-api.md +116 -0
  19. package/templates/beads-workflow-injection-git.md +108 -0
  20. package/templates/beads-workflow-injection.md +111 -0
  21. package/templates/frontend-reviews-requirement.md +61 -0
  22. package/templates/hooks/block-orchestrator-tools.sh +98 -0
  23. package/templates/hooks/clarify-vague-request.sh +39 -0
  24. package/templates/hooks/enforce-bead-for-supervisor.sh +32 -0
  25. package/templates/hooks/enforce-branch-before-edit.sh +47 -0
  26. package/templates/hooks/enforce-concise-response.sh +41 -0
  27. package/templates/hooks/enforce-sequential-dispatch.sh +63 -0
  28. package/templates/hooks/inject-discipline-reminder.sh +28 -0
  29. package/templates/hooks/log-dispatch-prompt.sh +39 -0
  30. package/templates/hooks/memory-capture.sh +104 -0
  31. package/templates/hooks/remind-inprogress.sh +14 -0
  32. package/templates/hooks/session-start.sh +121 -0
  33. package/templates/hooks/validate-completion.sh +131 -0
  34. package/templates/hooks/validate-epic-close.sh +84 -0
  35. package/templates/mcp.json.template +12 -0
  36. package/templates/memory/recall.sh +121 -0
  37. package/templates/settings.json +74 -0
  38. package/templates/skills/react-best-practices/SKILL.md +487 -0
  39. package/templates/skills/subagents-discipline/SKILL.md +127 -0
  40. package/templates/ui-constraints.md +76 -0
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ #
3
+ # UserPromptSubmit: Force clarification on vague requests + epic reminder
4
+ #
5
+ # Uses plain text stdout for context injection (per Claude Code docs)
6
+ #
7
+
8
+ INPUT=$(cat)
9
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty')
10
+ LENGTH=${#PROMPT}
11
+
12
+ if [[ $LENGTH -lt 50 ]]; then
13
+ cat << 'EOF'
14
+ <system-reminder>
15
+ STOP. This request is too short to act on safely.
16
+
17
+ BEFORE doing anything else, you MUST use the AskUserQuestion tool to clarify:
18
+ - What specific outcome does the user want?
19
+ - What files/components are involved?
20
+ - Are there any constraints or preferences?
21
+
22
+ Do NOT guess. Do NOT start working. Ask first.
23
+ </system-reminder>
24
+ EOF
25
+ elif [[ $LENGTH -lt 200 ]]; then
26
+ cat << 'EOF'
27
+ <system-reminder>
28
+ This request may be ambiguous. Consider using AskUserQuestion to clarify before proceeding.
29
+ </system-reminder>
30
+ EOF
31
+ fi
32
+
33
+ # Always remind about epic workflow
34
+ cat << 'EOF'
35
+ <cross-domain-check>
36
+ CRITICAL: If this task spans multiple supervisors, you MUST create an EPIC.
37
+ Cross-domain = Epic. No exceptions.
38
+ </cross-domain-check>
39
+ EOF
@@ -0,0 +1,32 @@
1
+ #!/bin/bash
2
+ #
3
+ # PreToolUse:Task - Enforce bead exists before supervisor dispatch
4
+ #
5
+ # All supervisors must have BEAD_ID in prompt.
6
+ # This ensures all work is tracked.
7
+ #
8
+
9
+ INPUT=$(cat)
10
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
11
+
12
+ [[ "$TOOL_NAME" != "Task" ]] && exit 0
13
+
14
+ SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // empty')
15
+ PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty')
16
+
17
+ # Only enforce for supervisors
18
+ [[ ! "$SUBAGENT_TYPE" =~ supervisor ]] && exit 0
19
+
20
+ # Exception: merge-supervisor is exempt from bead requirement
21
+ # Merge conflicts are incidental to other work, not tracked separately
22
+ [[ "$SUBAGENT_TYPE" == "merge-supervisor" ]] && exit 0
23
+
24
+ # Check for BEAD_ID in prompt
25
+ if [[ "$PROMPT" != *"BEAD_ID:"* ]]; then
26
+ cat << 'EOF'
27
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"<bead-required>\nAll supervisor work MUST be tracked with a bead.\n\n<action>\nFor standalone tasks:\n 1. bd create \"Task title\" -d \"Description\"\n 2. Dispatch with: BEAD_ID: {id}\n\nFor epic children:\n 1. bd create \"Epic\" -d \"...\" --type epic\n 2. bd create \"Child\" -d \"...\" --parent {EPIC_ID}\n 3. Dispatch with: BEAD_ID: {child_id}, EPIC_ID: {epic_id}\n</action>\n\nEach task creates its own worktree at .worktrees/bd-{BEAD_ID}/\n</bead-required>"}}
28
+ EOF
29
+ exit 0
30
+ fi
31
+
32
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ #
3
+ # PreToolUse: Block Edit/Write on main branch outside worktrees
4
+ #
5
+ # Supervisors must work in .worktrees/bd-{BEAD_ID}/ directories, not main.
6
+ # This prevents accidental commits to main directory.
7
+ #
8
+
9
+ INPUT=$(cat)
10
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
11
+
12
+ # Only check Edit and Write tools
13
+ [[ "$TOOL_NAME" != "Edit" ]] && [[ "$TOOL_NAME" != "Write" ]] && exit 0
14
+
15
+ # Get the file path being edited
16
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
17
+
18
+ # Allow if editing within .worktrees/ directory
19
+ if [[ "$FILE_PATH" == *"/.worktrees/"* ]] || [[ "$FILE_PATH" == *"\.worktrees\"* ]]; then
20
+ exit 0
21
+ fi
22
+
23
+ # Get current working directory
24
+ CWD=$(pwd)
25
+
26
+ # Allow if currently inside a .worktrees/ directory
27
+ if [[ "$CWD" == *"/.worktrees/"* ]] || [[ "$CWD" == *"\.worktrees\"* ]]; then
28
+ exit 0
29
+ fi
30
+
31
+ # Check current branch (if we're in a git repo outside worktrees)
32
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
33
+
34
+ # Block if on main or master (and not in a worktree)
35
+ if [[ "$CURRENT_BRANCH" == "main" ]] || [[ "$CURRENT_BRANCH" == "master" ]]; then
36
+ cat << EOF
37
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cannot edit files on $CURRENT_BRANCH branch. Supervisors must work in worktrees.
38
+
39
+ Create a worktree first using the API:
40
+ POST /api/git/worktree { repo_path, bead_id }
41
+
42
+ Then cd into .worktrees/bd-{BEAD_ID}/ to make changes."}}
43
+ EOF
44
+ exit 0
45
+ fi
46
+
47
+ exit 0
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ #
3
+ # PostToolUse: Enforce concise responses from subagents
4
+ #
5
+ # Subagents should return concise reports (max 10 lines, ~500 chars)
6
+ # This reduces context usage and keeps orchestrator focused.
7
+ #
8
+
9
+ INPUT=$(cat)
10
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
11
+
12
+ # Only check Task tool responses
13
+ [[ "$TOOL_NAME" != "Task" ]] && exit 0
14
+
15
+ # Get the tool response
16
+ RESPONSE=$(echo "$INPUT" | jq -r '.tool_result // empty')
17
+ [[ -z "$RESPONSE" ]] && exit 0
18
+
19
+ # Count lines and characters
20
+ LINE_COUNT=$(echo "$RESPONSE" | wc -l | tr -d ' ')
21
+ CHAR_COUNT=$(echo "$RESPONSE" | wc -c | tr -d ' ')
22
+
23
+ # Limits
24
+ MAX_LINES=10
25
+ MAX_CHARS=500
26
+
27
+ # Check limits (warn but don't block - agent already completed)
28
+ if [[ "$LINE_COUNT" -gt "$MAX_LINES" ]] || [[ "$CHAR_COUNT" -gt "$MAX_CHARS" ]]; then
29
+ # Log warning (PostToolUse can't deny, only add context)
30
+ cat << EOF
31
+ {
32
+ "hookSpecificOutput": {
33
+ "hookEventName": "PostToolUse",
34
+ "warning": "Subagent response exceeded limits (${LINE_COUNT} lines, ${CHAR_COUNT} chars). Target: ${MAX_LINES} lines, ${MAX_CHARS} chars. Consider asking agents for more concise reports."
35
+ }
36
+ }
37
+ EOF
38
+ exit 0
39
+ fi
40
+
41
+ exit 0
@@ -0,0 +1,63 @@
1
+ #!/bin/bash
2
+ #
3
+ # PreToolUse:Task - Enforce sequential dispatch and design doc existence
4
+ #
5
+ # For epic child tasks:
6
+ # 1. Blocks dispatch if task has unresolved blockers
7
+ # 2. Blocks dispatch if epic has design path but file doesn't exist
8
+ #
9
+
10
+ INPUT=$(cat)
11
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
12
+
13
+ [[ "$TOOL_NAME" != "Task" ]] && exit 0
14
+
15
+ SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // empty')
16
+ PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty')
17
+
18
+ # Only check for supervisors (not architect, scout, etc.)
19
+ [[ ! "$SUBAGENT_TYPE" =~ supervisor ]] && exit 0
20
+
21
+ # Worker-supervisor is exempt
22
+ [[ "$SUBAGENT_TYPE" == *"worker"* ]] && exit 0
23
+
24
+ # Extract BEAD_ID
25
+ BEAD_ID=$(echo "$PROMPT" | grep -oE "BEAD_ID: [A-Za-z0-9._-]+" | head -1 | sed 's/BEAD_ID: //')
26
+ [[ -z "$BEAD_ID" ]] && exit 0
27
+
28
+ # Block dispatch to closed/done beads - create a new bead instead
29
+ BEAD_STATUS=$(bd show "$BEAD_ID" --json 2>/dev/null | jq -r '.[0].status // empty')
30
+ if [[ "$BEAD_STATUS" == "closed" || "$BEAD_STATUS" == "done" ]]; then
31
+ cat << EOF
32
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"<closed-bead>\nBead ${BEAD_ID} is already ${BEAD_STATUS}. Do not reopen closed beads.\n\nCreate a new bead for follow-up work and relate it:\n\n bd create \"Fix: [description]\" -d \"Follow-up to ${BEAD_ID}: [details]\"\n # Returns: {NEW_ID}\n bd dep relate {NEW_ID} ${BEAD_ID}\n\nThen dispatch with the NEW bead ID.\n</closed-bead>"}}
33
+ EOF
34
+ exit 0
35
+ fi
36
+
37
+ # Check if this is an epic child (contains dot)
38
+ if [[ "$BEAD_ID" == *"."* ]]; then
39
+ # Extract EPIC_ID (everything before last dot)
40
+ EPIC_ID=$(echo "$BEAD_ID" | sed 's/\.[0-9]*$//')
41
+
42
+ # Check for unresolved blockers (exclude parent epic - it's not a real blocker)
43
+ BLOCKERS=$(bd dep list "$BEAD_ID" --json 2>/dev/null | jq -r --arg epic "$EPIC_ID" '.[] | select(.id != $epic and .status != "done" and .status != "closed") | .id' 2>/dev/null | tr '\n' ', ' | sed 's/,$//')
44
+
45
+ if [[ -n "$BLOCKERS" ]]; then
46
+ cat << EOF
47
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"<blocked-task>\nCannot dispatch ${BEAD_ID} - unresolved blockers: ${BLOCKERS}\n\nComplete blocking tasks first, then dispatch this one.\n\nUse: bd ready --json to see tasks with no blockers.\n</blocked-task>"}}
48
+ EOF
49
+ exit 0
50
+ fi
51
+
52
+ # Check design doc exists (if epic has design field)
53
+ DESIGN_PATH=$(bd show "$EPIC_ID" --json 2>/dev/null | jq -r '.[0].design // empty')
54
+
55
+ if [[ -n "$DESIGN_PATH" ]] && [[ ! -f "$DESIGN_PATH" ]]; then
56
+ cat << EOF
57
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"<design-doc-missing>\nEpic ${EPIC_ID} has design path '${DESIGN_PATH}' but file doesn't exist.\n\n<stop-and-think>\nBefore dispatching architect, verify you fully understand the epic:\n\n1. Are the requirements clear and unambiguous?\n2. Do you know the expected inputs/outputs?\n3. Are there edge cases or constraints to consider?\n4. Do you understand how this integrates with existing code?\n\nIf ANY ambiguity exists -> Use AskUserQuestion to clarify FIRST.\nDo NOT dispatch architect with vague requirements.\n</stop-and-think>\n\n<next-steps>\nIf requirements are CLEAR:\n Task(\n subagent_type=\"architect\",\n prompt=\"Create design doc for EPIC_ID: ${EPIC_ID}\n Output: ${DESIGN_PATH}\n \n [Provide clear, specific requirements]\"\n )\n\nIf requirements are UNCLEAR:\n AskUserQuestion(\n questions=[{\n \"question\": \"[Your specific clarifying question]\",\n \"header\": \"Clarify\",\n \"options\": [...],\n \"multiSelect\": false\n }]\n )\n</next-steps>\n</design-doc-missing>"}}
58
+ EOF
59
+ exit 0
60
+ fi
61
+ fi
62
+
63
+ exit 0
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ #
3
+ # PreToolUse: Inject discipline skill reminder for supervisor dispatches
4
+ #
5
+ # When orchestrator dispatches a supervisor via Task(), remind them to
6
+ # invoke the subagents-discipline skill at the start of implementation.
7
+ #
8
+
9
+ INPUT=$(cat)
10
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
11
+
12
+ # Only check Task tool
13
+ [[ "$TOOL_NAME" != "Task" ]] && exit 0
14
+
15
+ # Check if dispatching a supervisor
16
+ SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // empty')
17
+
18
+ # Only inject for supervisors (not code-reviewer, architect, etc.)
19
+ if [[ "$SUBAGENT_TYPE" == *"-supervisor"* ]]; then
20
+ cat << 'EOF'
21
+ <system-reminder>
22
+ SUPERVISOR DISPATCH: Before implementing, invoke `/subagents-discipline` skill.
23
+ This ensures verification-first development with DEMO blocks.
24
+ </system-reminder>
25
+ EOF
26
+ fi
27
+
28
+ exit 0
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ #
3
+ # PostToolUse:Task (async) - Auto-log dispatch prompts to bead comments
4
+ #
5
+ # When orchestrator dispatches a supervisor via Task(), capture the prompt
6
+ # and log it as a DISPATCH comment on the bead. This replaces manual
7
+ # INVESTIGATION logging — the dispatch prompt IS the investigation record.
8
+ #
9
+
10
+ INPUT=$(cat)
11
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
12
+
13
+ # Only process Task tool
14
+ [[ "$TOOL_NAME" != "Task" ]] && exit 0
15
+
16
+ # Extract subagent_type
17
+ SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // empty')
18
+
19
+ # Only log supervisor dispatches
20
+ [[ "$SUBAGENT_TYPE" != *"supervisor"* ]] && exit 0
21
+
22
+ # Extract prompt
23
+ PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty')
24
+ [[ -z "$PROMPT" ]] && exit 0
25
+
26
+ # Extract BEAD_ID from prompt
27
+ BEAD_ID=$(echo "$PROMPT" | grep -oE 'BEAD_ID: [A-Za-z0-9._-]+' | head -1 | sed 's/BEAD_ID: //')
28
+ [[ -z "$BEAD_ID" ]] && exit 0
29
+
30
+ # Truncate prompt at 2048 chars
31
+ TRUNCATED_PROMPT=$(echo "$PROMPT" | head -c 2048)
32
+
33
+ # Log dispatch to bead (fail silently)
34
+ # Prefix: DISPATCH_PROMPT — UI renders as collapsible "Prompt Used" entry
35
+ bd comment "$BEAD_ID" "DISPATCH_PROMPT [$SUBAGENT_TYPE]:
36
+
37
+ $TRUNCATED_PROMPT" 2>/dev/null || true
38
+
39
+ exit 0
@@ -0,0 +1,104 @@
1
+ #!/bin/bash
2
+ #
3
+ # PostToolUse:Bash (async) - Capture knowledge from bd comment commands
4
+ #
5
+ # Detects: bd comment {BEAD_ID} "LEARNED: ..."
6
+ # Extracts knowledge entries into .beads/memory/knowledge.jsonl
7
+ #
8
+
9
+ INPUT=$(cat)
10
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
11
+
12
+ # Only process Bash tool
13
+ [[ "$TOOL_NAME" != "Bash" ]] && exit 0
14
+
15
+ # Extract the command that was executed
16
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
17
+ [[ -z "$COMMAND" ]] && exit 0
18
+
19
+ # Only process bd comment commands containing knowledge markers
20
+ echo "$COMMAND" | grep -qE 'bd\s+comment\s+' || exit 0
21
+ echo "$COMMAND" | grep -qE 'LEARNED:' || exit 0
22
+
23
+ # Extract BEAD_ID (argument after "bd comment")
24
+ BEAD_ID=$(echo "$COMMAND" | sed -E 's/.*bd[[:space:]]+comment[[:space:]]+([A-Za-z0-9._-]+)[[:space:]]+.*/\1/')
25
+ [[ -z "$BEAD_ID" || "$BEAD_ID" == "$COMMAND" ]] && exit 0
26
+
27
+ # Extract the comment body (content inside quotes after bead ID)
28
+ COMMENT_BODY=$(echo "$COMMAND" | sed -E 's/.*bd[[:space:]]+comment[[:space:]]+[A-Za-z0-9._-]+[[:space:]]+["'\'']//' | sed -E 's/["'\''][[:space:]]*$//' | head -c 4096)
29
+ [[ -z "$COMMENT_BODY" ]] && exit 0
30
+
31
+ # Determine type and extract content (voluntary LEARNED only)
32
+ TYPE=""
33
+ CONTENT=""
34
+ if echo "$COMMENT_BODY" | grep -q "LEARNED:"; then
35
+ TYPE="learned"
36
+ CONTENT=$(echo "$COMMENT_BODY" | sed 's/.*LEARNED:[[:space:]]*//' | head -c 2048)
37
+ fi
38
+
39
+ [[ -z "$TYPE" || -z "$CONTENT" ]] && exit 0
40
+
41
+ # Generate key from content (type + slugified first 60 chars)
42
+ SLUG=$(echo "$CONTENT" | head -c 60 | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//')
43
+ KEY="${TYPE}-${SLUG}"
44
+
45
+ # Detect source agent from CWD or transcript context
46
+ SOURCE="orchestrator"
47
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
48
+ if echo "$CWD" | grep -q '\.worktrees/'; then
49
+ # Inside a worktree = supervisor is running
50
+ SOURCE="supervisor"
51
+ fi
52
+
53
+ # Build tags array - start with type tag
54
+ TAGS_ARRAY=("$TYPE")
55
+
56
+ # Scan content for known tech keywords and add matching tags
57
+ for tag in swift swiftui appkit menubar api security test database \
58
+ networking ui layout performance crash bug fix workaround \
59
+ gotcha pattern convention architecture auth middleware \
60
+ async concurrency model protocol adapter scanner engine; do
61
+ if echo "$CONTENT" | grep -qi "$tag"; then
62
+ TAGS_ARRAY+=("$tag")
63
+ fi
64
+ done
65
+
66
+ # Convert tags array to JSON
67
+ TAGS_JSON=$(printf '%s\n' "${TAGS_ARRAY[@]}" | jq -R . | jq -s .)
68
+
69
+ # Get timestamp
70
+ TS=$(date +%s)
71
+
72
+ # Build JSON entry with proper escaping
73
+ ENTRY=$(jq -cn \
74
+ --arg key "$KEY" \
75
+ --arg type "$TYPE" \
76
+ --arg content "$CONTENT" \
77
+ --arg source "$SOURCE" \
78
+ --argjson tags "$TAGS_JSON" \
79
+ --argjson ts "$TS" \
80
+ --arg bead "$BEAD_ID" \
81
+ '{key: $key, type: $type, content: $content, source: $source, tags: $tags, ts: $ts, bead: $bead}')
82
+
83
+ # Validate JSON
84
+ [[ -z "$ENTRY" ]] && exit 0
85
+ echo "$ENTRY" | jq . >/dev/null 2>&1 || exit 0
86
+
87
+ # Resolve memory directory
88
+ MEMORY_DIR="${CLAUDE_PROJECT_DIR:-.}/.beads/memory"
89
+ mkdir -p "$MEMORY_DIR"
90
+ KNOWLEDGE_FILE="$MEMORY_DIR/knowledge.jsonl"
91
+
92
+ # Append entry
93
+ echo "$ENTRY" >> "$KNOWLEDGE_FILE"
94
+
95
+ # Rotation: archive oldest 500 when file exceeds 1000 lines
96
+ LINE_COUNT=$(wc -l < "$KNOWLEDGE_FILE" 2>/dev/null | tr -d ' ')
97
+ if [[ "$LINE_COUNT" -gt 1000 ]]; then
98
+ ARCHIVE_FILE="$MEMORY_DIR/knowledge.archive.jsonl"
99
+ head -500 "$KNOWLEDGE_FILE" >> "$ARCHIVE_FILE"
100
+ tail -n +501 "$KNOWLEDGE_FILE" > "$KNOWLEDGE_FILE.tmp"
101
+ mv "$KNOWLEDGE_FILE.tmp" "$KNOWLEDGE_FILE"
102
+ fi
103
+
104
+ exit 0
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+ #
3
+ # PreToolUse:Task - Soft reminder to set bead status before dispatch
4
+ #
5
+
6
+ INPUT=$(cat)
7
+ PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty')
8
+
9
+ # Only remind if dispatching a bead task (prompt contains BEAD_ID)
10
+ if [[ "$PROMPT" == *"BEAD_ID:"* ]]; then
11
+ echo "IMPORTANT: Before dispatching, ensure bead is in_progress: bd update {BEAD_ID} --status in_progress"
12
+ fi
13
+
14
+ exit 0
@@ -0,0 +1,121 @@
1
+ #!/bin/bash
2
+ #
3
+ # SessionStart: Show full task context for orchestrator
4
+ #
5
+
6
+ BEADS_DIR="$CLAUDE_PROJECT_DIR/.beads"
7
+
8
+ if [[ ! -d "$BEADS_DIR" ]]; then
9
+ echo "No .beads directory found. Run 'bd init' to initialize."
10
+ exit 0
11
+ fi
12
+
13
+ # Check if bd is available
14
+ if ! command -v bd &>/dev/null; then
15
+ echo "beads CLI (bd) not found. Install from: https://github.com/steveyegge/beads"
16
+ exit 0
17
+ fi
18
+
19
+ # ============================================================
20
+ # Dirty Parent Check - Warn if main directory has uncommitted changes
21
+ # ============================================================
22
+ REPO_ROOT=$(git -C "$CLAUDE_PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null)
23
+ if [[ -n "$REPO_ROOT" ]]; then
24
+ DIRTY=$(git -C "$REPO_ROOT" status --porcelain 2>/dev/null)
25
+ if [[ -n "$DIRTY" ]]; then
26
+ echo "⚠️ WARNING: Main directory has uncommitted changes."
27
+ echo " Agents should only work in .worktrees/"
28
+ echo ""
29
+ fi
30
+ fi
31
+
32
+ # ============================================================
33
+ # Auto-cleanup: Detect merged PRs and cleanup worktrees
34
+ # ============================================================
35
+ WORKTREES_DIR="$CLAUDE_PROJECT_DIR/.worktrees"
36
+ if [[ -d "$WORKTREES_DIR" ]]; then
37
+ for worktree in $(git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | grep "^worktree.*\.worktrees/bd-" | awk '{print $2}'); do
38
+ BEAD_ID=$(basename "$worktree" | sed 's/bd-//')
39
+ BRANCH=$(basename "$worktree")
40
+
41
+ # Check if branch was merged to main
42
+ if git -C "$REPO_ROOT" branch --merged main 2>/dev/null | grep -q "$BRANCH"; then
43
+ echo "✓ $BRANCH was merged - consider cleaning up"
44
+ echo " Run: git worktree remove \"$worktree\" && bd close \"$BEAD_ID\""
45
+ echo ""
46
+ fi
47
+ done
48
+ fi
49
+
50
+ # ============================================================
51
+ # Open PR Reminder
52
+ # ============================================================
53
+ if command -v gh &>/dev/null; then
54
+ OPEN_PRS=$(gh pr list --author "@me" --state open --json number,title,headRefName 2>/dev/null)
55
+ if [[ -n "$OPEN_PRS" && "$OPEN_PRS" != "[]" ]]; then
56
+ echo "📋 You have open PRs:"
57
+ echo "$OPEN_PRS" | jq -r '.[] | " #\(.number) \(.title) (\(.headRefName))"' 2>/dev/null
58
+ echo ""
59
+ fi
60
+ fi
61
+
62
+ echo ""
63
+ echo "## Task Status"
64
+ echo ""
65
+
66
+ # Show in-progress beads first (highest priority)
67
+ IN_PROGRESS=$(bd list --status in_progress 2>/dev/null | head -5)
68
+ if [[ -n "$IN_PROGRESS" ]]; then
69
+ echo "### In Progress (resume these):"
70
+ echo "$IN_PROGRESS"
71
+ echo ""
72
+ fi
73
+
74
+ # Show ready (unblocked) beads
75
+ READY=$(bd ready 2>/dev/null | head -5)
76
+ if [[ -n "$READY" ]]; then
77
+ echo "### Ready (no blockers):"
78
+ echo "$READY"
79
+ echo ""
80
+ fi
81
+
82
+ # Show blocked beads
83
+ BLOCKED=$(bd blocked 2>/dev/null | head -3)
84
+ if [[ -n "$BLOCKED" ]]; then
85
+ echo "### Blocked:"
86
+ echo "$BLOCKED"
87
+ echo ""
88
+ fi
89
+
90
+ # Show stale beads (no activity in 3 days)
91
+ STALE=$(bd stale --days 3 2>/dev/null | head -3)
92
+ if [[ -n "$STALE" ]]; then
93
+ echo "### Stale (no activity in 3 days):"
94
+ echo "$STALE"
95
+ echo ""
96
+ fi
97
+
98
+ # If nothing found
99
+ if [[ -z "$IN_PROGRESS" && -z "$READY" && -z "$BLOCKED" && -z "$STALE" ]]; then
100
+ echo "No active beads. Create one with: bd create \"Task title\" -d \"Description\""
101
+ fi
102
+
103
+ # ============================================================
104
+ # Knowledge Base - Surface recent learnings
105
+ # ============================================================
106
+ KNOWLEDGE_FILE="$BEADS_DIR/memory/knowledge.jsonl"
107
+ if [[ -f "$KNOWLEDGE_FILE" && -s "$KNOWLEDGE_FILE" ]]; then
108
+ TOTAL_ENTRIES=$(wc -l < "$KNOWLEDGE_FILE" | tr -d ' ')
109
+ echo ""
110
+ echo "## Recent Knowledge ($TOTAL_ENTRIES entries)"
111
+ echo ""
112
+ # Show 5 most recent, deduplicated by key (latest wins)
113
+ tail -20 "$KNOWLEDGE_FILE" | jq -s '
114
+ group_by(.key) | map(max_by(.ts)) | sort_by(-.ts) | .[0:5] | .[] |
115
+ " [\(.type | ascii_upcase | .[0:5])] \(.content | .[0:100]) (\(.source))"
116
+ ' -r 2>/dev/null
117
+ echo ""
118
+ echo " Search: .beads/memory/recall.sh \"keyword\""
119
+ fi
120
+
121
+ echo ""
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ #
3
+ # SubagentStop: Enforce bead lifecycle - work verification
4
+ #
5
+
6
+ INPUT=$(cat)
7
+ AGENT_TRANSCRIPT=$(echo "$INPUT" | jq -r '.agent_transcript_path // empty')
8
+ MAIN_TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
9
+ AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
10
+
11
+ [[ -z "$AGENT_TRANSCRIPT" || ! -f "$AGENT_TRANSCRIPT" ]] && echo '{"decision":"approve"}' && exit 0
12
+
13
+ # Extract last assistant text response
14
+ LAST_RESPONSE=$(tail -200 "$AGENT_TRANSCRIPT" | jq -rs '
15
+ [.[] | select(.message?.role == "assistant" and .message?.content != null)
16
+ | .message.content[] | select(.text != null) | .text] | last // ""
17
+ ' 2>/dev/null || echo "")
18
+
19
+ # === LAYER 1: Extract subagent_type from transcript (fail open) ===
20
+ SUBAGENT_TYPE=""
21
+ if [[ -n "$AGENT_ID" && -n "$MAIN_TRANSCRIPT" && -f "$MAIN_TRANSCRIPT" ]]; then
22
+ PARENT_TOOL_USE_ID=$(grep "\"agentId\":\"$AGENT_ID\"" "$MAIN_TRANSCRIPT" 2>/dev/null | head -1 | jq -r '.parentToolUseID // empty' 2>/dev/null)
23
+ if [[ -n "$PARENT_TOOL_USE_ID" ]]; then
24
+ SUBAGENT_TYPE=$(grep "\"id\":\"$PARENT_TOOL_USE_ID\"" "$MAIN_TRANSCRIPT" 2>/dev/null | \
25
+ grep '"name":"Task"' | \
26
+ jq -r '.message.content[]? | select(.type == "tool_use" and .id == "'"$PARENT_TOOL_USE_ID"'") | .input.subagent_type // empty' 2>/dev/null | \
27
+ head -1)
28
+ fi
29
+ fi
30
+
31
+ # === LAYER 2: Check completion format (backup detection) ===
32
+ HAS_BEAD_COMPLETE=$(echo "$LAST_RESPONSE" | grep -cE "BEAD.*COMPLETE" 2>/dev/null || true)
33
+ HAS_WORKTREE_OR_BRANCH=$(echo "$LAST_RESPONSE" | grep -cE "(Worktree:|Branch:).*bd-" 2>/dev/null || true)
34
+ [[ -z "$HAS_BEAD_COMPLETE" ]] && HAS_BEAD_COMPLETE=0
35
+ [[ -z "$HAS_WORKTREE_OR_BRANCH" ]] && HAS_WORKTREE_OR_BRANCH=0
36
+
37
+ # Determine if this is a supervisor (Layer 1) or has completion format (Layer 2)
38
+ IS_SUPERVISOR="false"
39
+ [[ "$SUBAGENT_TYPE" == *"supervisor"* ]] && IS_SUPERVISOR="true"
40
+
41
+ NEEDS_VERIFICATION="false"
42
+ [[ "$IS_SUPERVISOR" == "true" ]] && NEEDS_VERIFICATION="true"
43
+ [[ "$HAS_BEAD_COMPLETE" -ge 1 && "$HAS_WORKTREE_OR_BRANCH" -ge 1 ]] && NEEDS_VERIFICATION="true"
44
+
45
+ # Skip verification if not needed
46
+ [[ "$NEEDS_VERIFICATION" == "false" ]] && echo '{"decision":"approve"}' && exit 0
47
+
48
+ # Worker supervisor is exempt
49
+ [[ "$SUBAGENT_TYPE" == *"worker"* ]] && echo '{"decision":"approve"}' && exit 0
50
+
51
+ # === VERIFICATION CHECKS ===
52
+
53
+ # Check 1: Completion format required for supervisors
54
+ if [[ "$IS_SUPERVISOR" == "true" ]] && [[ "$HAS_BEAD_COMPLETE" -lt 1 || "$HAS_WORKTREE_OR_BRANCH" -lt 1 ]]; then
55
+ cat << 'EOF'
56
+ {"decision":"block","reason":"Work verification failed: completion report missing.\n\nRequired format:\nBEAD {BEAD_ID} COMPLETE\nWorktree: .worktrees/bd-{BEAD_ID}\nFiles: [list]\nTests: pass\nSummary: [1 sentence]"}
57
+ EOF
58
+ exit 0
59
+ fi
60
+
61
+ # Extract BEAD_ID from response
62
+ BEAD_ID_FROM_RESPONSE=$(echo "$LAST_RESPONSE" | grep -oE "BEAD [A-Za-z0-9._-]+" | head -1 | awk '{print $2}')
63
+ IS_EPIC_CHILD="false"
64
+ [[ "$BEAD_ID_FROM_RESPONSE" == *"."* ]] && IS_EPIC_CHILD="true"
65
+
66
+ # Check 2: Comment required
67
+ HAS_COMMENT=$(grep -c '"bd comment\|"command":"bd comment' "$AGENT_TRANSCRIPT" 2>/dev/null) || HAS_COMMENT=0
68
+ if [[ "$HAS_COMMENT" -lt 1 ]]; then
69
+ cat << 'EOF'
70
+ {"decision":"block","reason":"Work verification failed: no comment on bead.\n\nRun: bd comment {BEAD_ID} \"Completed: [summary]\""}
71
+ EOF
72
+ exit 0
73
+ fi
74
+
75
+ # Check 3: Worktree verification
76
+ REPO_ROOT=$(cd "$(git rev-parse --git-common-dir)/.." 2>/dev/null && pwd)
77
+ WORKTREE_PATH="$REPO_ROOT/.worktrees/bd-${BEAD_ID_FROM_RESPONSE}"
78
+
79
+ if [[ ! -d "$WORKTREE_PATH" ]]; then
80
+ cat << 'EOF'
81
+ {"decision":"block","reason":"Work verification failed: worktree not found.\n\nCreate worktree first via API."}
82
+ EOF
83
+ exit 0
84
+ fi
85
+
86
+ # Check 4: Uncommitted changes
87
+ UNCOMMITTED=$(git -C "$WORKTREE_PATH" status --porcelain 2>/dev/null)
88
+ if [[ -n "$UNCOMMITTED" ]]; then
89
+ cat << 'EOF'
90
+ {"decision":"block","reason":"Work verification failed: uncommitted changes.\n\nRun in worktree:\n git add -A && git commit -m \"...\""}
91
+ EOF
92
+ exit 0
93
+ fi
94
+
95
+ # Check 5: Remote push
96
+ HAS_REMOTE=$(git -C "$WORKTREE_PATH" remote get-url origin 2>/dev/null)
97
+ if [[ -n "$HAS_REMOTE" ]]; then
98
+ BRANCH="bd-${BEAD_ID_FROM_RESPONSE}"
99
+ REMOTE_EXISTS=$(git -C "$WORKTREE_PATH" ls-remote --heads origin "$BRANCH" 2>/dev/null)
100
+ if [[ -z "$REMOTE_EXISTS" ]]; then
101
+ cat << 'EOF'
102
+ {"decision":"block","reason":"Work verification failed: branch not pushed.\n\nRun: git push -u origin bd-{BEAD_ID}"}
103
+ EOF
104
+ exit 0
105
+ fi
106
+ fi
107
+
108
+ # Check 6: Bead status
109
+ BEAD_STATUS=$(bd show "$BEAD_ID_FROM_RESPONSE" --json 2>/dev/null | jq -r '.[0].status // "unknown"')
110
+ EXPECTED_STATUS="inreview"
111
+ # Epic children also use inreview (done status not supported in bd)
112
+ if [[ "$BEAD_STATUS" != "$EXPECTED_STATUS" ]]; then
113
+ cat << EOF
114
+ {"decision":"block","reason":"Work verification failed: bead status is '${BEAD_STATUS}'.\n\nRun: bd update ${BEAD_ID_FROM_RESPONSE} --status ${EXPECTED_STATUS}"}
115
+ EOF
116
+ exit 0
117
+ fi
118
+
119
+ # Check 7: Verbosity limit
120
+ DECODED_RESPONSE=$(printf '%b' "$LAST_RESPONSE")
121
+ LINE_COUNT=$(echo "$DECODED_RESPONSE" | wc -l | tr -d ' ')
122
+ CHAR_COUNT=${#DECODED_RESPONSE}
123
+
124
+ if [[ "$LINE_COUNT" -gt 15 ]] || [[ "$CHAR_COUNT" -gt 800 ]]; then
125
+ cat << EOF
126
+ {"decision":"block","reason":"Work verification failed: response too verbose (${LINE_COUNT} lines, ${CHAR_COUNT} chars). Max: 15 lines, 800 chars."}
127
+ EOF
128
+ exit 0
129
+ fi
130
+
131
+ echo '{"decision":"approve"}'