From fb4ad00ae43009683d9a208add55bd3345d34a9e Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:02:50 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 14 ++ README.md | 3 + commands/execute.md | 12 ++ commands/plan.md | 54 ++++++ hooks/hooks.json | 36 ++++ hooks/post-tool-use.sh | 266 +++++++++++++++++++++++++++++ hooks/pre-tool-use.sh | 339 +++++++++++++++++++++++++++++++++++++ hooks/session-start.sh | 89 ++++++++++ plugin.lock.json | 69 ++++++++ 9 files changed, 882 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/execute.md create mode 100644 commands/plan.md create mode 100644 hooks/hooks.json create mode 100755 hooks/post-tool-use.sh create mode 100755 hooks/pre-tool-use.sh create mode 100755 hooks/session-start.sh create mode 100644 plugin.lock.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a03afbb --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "rgw", + "description": "Requirement Gathering Workflow - A structured workflow for gathering requirements, generating tasks, and executing them systematically", + "version": "1.0.11", + "author": { + "name": "Byborg" + }, + "commands": [ + "./commands" + ], + "hooks": [ + "./hooks" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9b441f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# rgw + +Requirement Gathering Workflow - A structured workflow for gathering requirements, generating tasks, and executing them systematically diff --git a/commands/execute.md b/commands/execute.md new file mode 100644 index 0000000..1edb47f --- /dev/null +++ b/commands/execute.md @@ -0,0 +1,12 @@ +Executes the previously created tasklist. + +## Variables +- `WorkflowTaskExecution`: `${CLAUDE_PLUGIN_ROOT}/context/workflow/task-execution.md` + +## Workflow +- follow the task execution workflow as described in +- tell the user in a clearly visible way, that you understand and are following this workflow, and list all the arguments received +- IF an argument was received, read $1 and execute strictly following the execution workflow +- IF no arguments were given, list all tasks (`task-XXX.yaml` files, relative to project root), and their statuses. + - if there are no tasks found, stop here and suggest the User to plan first. + - execute each task sequentially strictly following the execution workflow diff --git a/commands/plan.md b/commands/plan.md new file mode 100644 index 0000000..1101029 --- /dev/null +++ b/commands/plan.md @@ -0,0 +1,54 @@ +Based on user defined requirements gather questions to clarify. + +## Variables + +### Requirement Gathering Variables +- `WorkflowRequirementGathering`: `${CLAUDE_PLUGIN_ROOT}/context/workflow/requirement-gathering.md` +- `CommunicationStandards`: `${CLAUDE_PLUGIN_ROOT}/context/standards/communication-standards.md` +- `RequirementsSyntax`: `${CLAUDE_PLUGIN_ROOT}/context/syntaxes/requirements-syntax.md` + +### Task Generation Variables +- `WorkflowTaskGeneration`: `${CLAUDE_PLUGIN_ROOT}/context/workflow/task-generation.md` +- `TaskSyntax`: `${CLAUDE_PLUGIN_ROOT}/context/syntaxes/task-syntax.md` +- `CodingStandards`: `${CLAUDE_PLUGIN_ROOT}/context/standards/coding-standards.md` + +### Replan Variables +- `WorkflowReplan`: `${CLAUDE_PLUGIN_ROOT}/context/workflow/replan.md` + +## Workflow + +### Phase 1: Prerequisites Check +- Tell the user in a clearly visible way, that you understand and are following this workflow. +- Check if `requirements.yaml` exists in the project root using a simple bash command, do not read it's content yet. +- If `requirements.yaml` EXISTS: + - Use the AskUserQuestion tool to present these options: + - **Option 1: Fresh Plan** - Create a new plan from scratch (will delete existing requirements.yaml and all task-*.yaml files) + - **Option 2: Replan** - Update existing requirements by reviewing and adding new aspects (will regenerate all task files based on updated requirements) + - Based on user's choice: + - If **Fresh Plan**: Delete `requirements.yaml` and all `task-*.yaml` files, then proceed with normal planning + - If **Replan**: + - Follow the REPLAN workflow as defined in +- If `requirements.yaml` DOES NOT EXIST: + - Proceed with normal planning workflow + +### Phase 2: Planning Process +- Wait for the User to ask a question or give you an instruction! +- If this is a FRESH PLAN or NO EXISTING requirements: + - Start by gathering requirements, as defined in + - Once all requirements are gathered, present a summary to the user + - **IMPORTANT**: Request explicit approval from user on the requirements gathered + - If user approves, update `requirements.yaml` (in project root) to represent the latest requirement-gathering state + - **STOP HERE** - Do NOT proceed to Phase 3 automatically +- If this is a REPLAN: + - Follow the replan workflow as defined in + - Iterate through existing requirements and gather additional/modified requirements + - Once replanning is complete, present the updated requirements to the user + - **IMPORTANT**: Request explicit approval from user on the updated requirements + - If user approves, delete ALL existing `task-*.yaml` files and update `requirements.yaml` with the enhanced requirements + - **STOP HERE** - Do NOT proceed to Phase 3 automatically + +### Phase 3: Task Generation (User Approval Required) +- **IMPORTANT**: This phase requires explicit user permission. Ask: "Requirements gathering is complete. Would you like me to proceed with generating the task list?" +- Only proceed if the user explicitly approves task generation +- Create a list of tasks, as defined in +- After the task list is generated, your work is done. DO NOT start working on the tasks! \ No newline at end of file diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..fecb8c5 --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,36 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Write|Edit|mcp__serena__create_text_file|mcp__serena__replace_regex|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|mcp__serena__create_text_file|mcp__serena__replace_regex|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.sh" + } + ] + } + ] + } +} diff --git a/hooks/post-tool-use.sh b/hooks/post-tool-use.sh new file mode 100755 index 0000000..6f6d7b5 --- /dev/null +++ b/hooks/post-tool-use.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================ +# Load Logger Library +# ============================================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/logger.sh" + +# ============================================================================ +# PART -1: Check if required commands are installed +# ============================================================================ + +if ! command -v yq &> /dev/null; then + output=$(cat <<'EOF' +{ + "decision": "block", + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": "yq is not installed. Install it to enable hook functionality.\n macOS: brew install yq\n Linux: https://github.com/mikefarah/yq#install" + } +} +EOF +) + log_hook_output "post-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 +fi + +if ! command -v node &> /dev/null; then + output=$(cat <<'EOF' +{ + "decision": "block", + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": "Node.js is not installed. Install it to enable hook functionality.\n macOS: brew install node\n Linux: https://nodejs.org/en/download/package-manager" + } +} +EOF +) + log_hook_output "post-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 +fi + +if ! command -v npx &> /dev/null; then + output=$(cat <<'EOF' +{ + "decision": "block", + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": "npx is not installed. Install it to enable hook functionality.\n npm install -g npx" + } +} +EOF +) + log_hook_output "post-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 +fi + +# Determine which JSON command to use (prefer installed json, fallback to npx) +if command -v json &> /dev/null; then + JSON_CMD="json" +else + JSON_CMD="npx -y json" +fi + +# Read JSON input +json=$(cat) + +# Extract file path and tool name +# For Write/Edit tools, the file path is in tool_response.filePath +# For Serena MCP tools, we need to get it from the tool_input.relative_path +file_path=$(echo "$json" | $JSON_CMD tool_response.filePath 2>/dev/null || echo "") +if [[ -z "$file_path" ]]; then + file_path=$(echo "$json" | $JSON_CMD tool_input.relative_path 2>/dev/null || echo "") +fi +tool_name=$(echo "$json" | $JSON_CMD tool_name 2>/dev/null || echo "") + +# ============================================================================ +# PART 0: Track Changed Files in In-Progress Task +# ============================================================================ + +if [[ -n "$file_path" ]]; then + # Skip tracking task yaml files and requirements.yaml + filename=$(basename "$file_path") + if [[ "$filename" =~ ^task-.*\.ya?ml$ ]] || [[ "$filename" == "requirements.yaml" ]]; then + # Don't track these files + : + else + # Find the task file with "status: in progress" + in_progress_task="" + for task_file in task-*.yaml task-*.yml; do + if [[ -f "$task_file" ]]; then + # Check if task has "in progress" status using yq + task_status=$(yq -r '.status // ""' "$task_file" 2>/dev/null || echo "") + # Remove quotes if present + if [[ "$task_status" =~ ^[\"\'](.*)[\"\']$ ]]; then + task_status="${BASH_REMATCH[1]}" + fi + + if [[ "$task_status" == "in progress" ]]; then + in_progress_task="$task_file" + break + fi + fi + done + + # If we found an in-progress task, add the file to changed_files if not already there + if [[ -n "$in_progress_task" ]]; then + # Check if file is already in changed_files array + file_exists=$(yq -r ".changed_files // [] | map(select(. == \"$file_path\")) | length" "$in_progress_task" 2>/dev/null || echo "0") + + if [[ "$file_exists" == "0" ]]; then + # Add file to changed_files array + yq -i ".changed_files += [\"$file_path\"]" "$in_progress_task" + fi + fi + fi +fi + +# ============================================================================ +# PART 1: Verify Requirements +# ============================================================================ + +if [[ -n "$file_path" ]] && [[ "$file_path" == *"requirements.yaml" ]]; then + if [[ -f "$file_path" ]]; then + if yq -e '.complete == true' "$file_path" >/dev/null 2>&1; then + # Step 1: Get full Claude output + claude_output=$(claude -p "read and execute ${CLAUDE_PLUGIN_ROOT}/context/verify-requirements.md") + + # Step 2: Extract YAML from output, removing any markdown code fences + yaml=$(echo "$claude_output" | awk '/^passed:/{flag=1} flag' | sed '/^```/d') + + passed=$(echo "$yaml" | yq -r '.passed') + remarks=$(echo "$yaml" | yq -r '.remarks[]?') + + if [[ "$passed" == "false" ]]; then + # Use jq to properly construct JSON with escaped content + output=$(jq -n \ + --arg remarks "$remarks" \ + --arg claude_output "$claude_output" \ + '{ + decision: "block", + hookSpecificOutput: { + hookEventName: "PostToolUse", + additionalContext: ("Requirements verification failed and needs fixing with the following reasons:\n" + $remarks + "\n\nFull verification output:\n" + $claude_output) + } + }') + log_hook_output "post-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 + else + # Log successful verification + success_output=$(cat < /dev/null; then + if ! timeout 30 $cmd "$file_path" 1>&2; then + # Use jq to properly construct JSON with escaped content + output=$(jq -n \ + --arg cmd "$cmd" \ + --arg file_path "$file_path" \ + '{ + decision: "block", + hookSpecificOutput: { + hookEventName: "PostToolUse", + additionalContext: ("Command failed: " + $cmd + " for " + $file_path) + } + }') + log_hook_output "post-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 + fi + fi + done +fi + +# All checks passed - log successful execution with no blocking output +log_hook_output "post-tool-use" "" +exit 0 diff --git a/hooks/pre-tool-use.sh b/hooks/pre-tool-use.sh new file mode 100755 index 0000000..fb06e82 --- /dev/null +++ b/hooks/pre-tool-use.sh @@ -0,0 +1,339 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================ +# Load Logger Library +# ============================================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/logger.sh" + +# ============================================================================ +# PART -1: Check if required commands are installed +# ============================================================================ + +if ! command -v yq &> /dev/null; then + output=$(cat <<'EOF' +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "yq is not installed. Install it to enable hook functionality.\n macOS: brew install yq\n Linux: https://github.com/mikefarah/yq#install" + } +} +EOF +) + log_hook_output "pre-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 +fi + +if ! command -v node &> /dev/null; then + output=$(cat <<'EOF' +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "Node.js is not installed. Install it to enable hook functionality.\n macOS: brew install node\n Linux: https://nodejs.org/en/download/package-manager" + } +} +EOF +) + log_hook_output "pre-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 +fi + +if ! command -v npx &> /dev/null; then + output=$(cat <<'EOF' +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "npx is not installed. Install it to enable hook functionality.\n npm install -g npx" + } +} +EOF +) + log_hook_output "pre-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 +fi + +# Determine which JSON command to use (prefer installed json, fallback to npx) +if command -v json &> /dev/null; then + JSON_CMD="json" +else + JSON_CMD="npx -y json" +fi + +# Read JSON input +json=$(cat) + +# Extract file path (try both file_path and relative_path for Serena MCP tools) +file_path=$(echo "$json" | $JSON_CMD tool_input.file_path 2>/dev/null || echo "") +if [[ -z "$file_path" ]]; then + file_path=$(echo "$json" | $JSON_CMD tool_input.relative_path 2>/dev/null || echo "") +fi + +# ============================================================================ +# PART 2: Verify Task +# ============================================================================ + +if [[ "$file_path" =~ task-[0-9]+\.yaml$ ]]; then + if [[ -f "$file_path" ]]; then + # Extract old and new status from the hook JSON + old_string=$(echo "$json" | $JSON_CMD tool_input.old_string 2>/dev/null || echo "") + new_string=$(echo "$json" | $JSON_CMD tool_input.new_string 2>/dev/null || echo "") + + current_status="" + new_status="" + old_has_status=false + new_has_status=false + + # Parse status from old_string (format: "status: " or "status: \"\"" or "status: ''") + # Use grep to extract just the status line, handling multiline strings + # Use grep -m 1 to only get the first match in case of multiline content + if status_line=$(echo "$old_string" | grep -m 1 -E '^status:[[:space:]]*'); then + # Match quoted or unquoted status values (including multi-word statuses) + if [[ "$status_line" =~ ^status:[[:space:]]*\"([^\"]+)\" ]]; then + current_status="${BASH_REMATCH[1]}" + old_has_status=true + elif [[ "$status_line" =~ ^status:[[:space:]]*\'([^\']+)\' ]]; then + current_status="${BASH_REMATCH[1]}" + old_has_status=true + elif [[ "$status_line" =~ ^status:[[:space:]]*(.+)$ ]]; then + # For unquoted values, capture everything after "status:" until end of line + current_status=$(echo "${BASH_REMATCH[1]}" | sed 's/[[:space:]]*$//') + old_has_status=true + fi + fi + + # Parse status from new_string (format: "status: " or "status: \"\"" or "status: ''") + # Use grep to extract just the status line, handling multiline strings + # Use grep -m 1 to only get the first match in case of multiline content + if status_line=$(echo "$new_string" | grep -m 1 -E '^status:[[:space:]]*'); then + # Match quoted or unquoted status values (including multi-word statuses) + if [[ "$status_line" =~ ^status:[[:space:]]*\"([^\"]+)\" ]]; then + new_status="${BASH_REMATCH[1]}" + new_has_status=true + elif [[ "$status_line" =~ ^status:[[:space:]]*\'([^\']+)\' ]]; then + new_status="${BASH_REMATCH[1]}" + new_has_status=true + elif [[ "$status_line" =~ ^status:[[:space:]]*(.+)$ ]]; then + # For unquoted values, capture everything after "status:" until end of line + new_status=$(echo "${BASH_REMATCH[1]}" | sed 's/[[:space:]]*$//') + new_has_status=true + fi + fi + + # Check for status field removal + if [[ "$old_has_status" == true && "$new_has_status" == false ]]; then + output=$(cat <<'EOF' +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "Removal of status field is not allowed" + } +} +EOF +) + log_hook_output "pre-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 + fi + + # Define valid transitions + # Check if transition is valid + if [[ -n "$new_status" && "$new_status" != "$current_status" ]]; then + valid_transition=false + valid_next=() + + case "$current_status" in + "") + valid_next=("to do") + ;; + "to do") + valid_next=("in progress") + ;; + "in progress") + valid_next=("under review") + ;; + "under review") + valid_next=("done" "in progress") + ;; + "done") + valid_next=() + ;; + *) + valid_next=() + ;; + esac + + # Check if new_status is in valid_next array + for allowed in "${valid_next[@]}"; do + [[ "$new_status" == "$allowed" ]] && valid_transition=true && break + done + + # Format expected_next for error message + expected_next=$(IFS=' or '; echo "${valid_next[*]}") + [[ -z "$expected_next" ]] && expected_next="" + + if [[ "$valid_transition" == false ]]; then + output=$(cat <}' → '$new_status'\\nExpected: '${current_status:-}' → '${expected_next}'" + } +} +EOF +) + log_hook_output "pre-tool-use" "$output" + check_and_echo_block_reason "$output" + echo "$output" + exit 1 + fi + fi + + # Function to extract status from file, handling quotes + extract_file_status() { + local file="$1" + local raw_status=$(yq -r '.status // ""' "$file") + # Remove surrounding quotes if present + if [[ "$raw_status" =~ ^[\"\'](.*)[\"\']$ ]]; then + echo "${BASH_REMATCH[1]}" + else + echo "$raw_status" + fi + } + + # Specific status validations + case "$new_status" in + "in progress") + # Check if any OTHER task is in "under review" status + # (Allow setting the same file back to "in progress" from "under review") + current_basename=$(basename "$file_path") + for task_file in task-*.yaml; do + if [[ "$task_file" != "$current_basename" && -f "$task_file" ]]; then + other_status=$(extract_file_status "$task_file") + if [[ "$other_status" == "under review" ]]; then + output=$(cat </dev/null || echo "") + + # Check if the new status will be "done" + new_status="" + if status_line=$(echo "$new_string" | grep -m 1 -E '^status:[[:space:]]*'); then + # Match quoted or unquoted status values (including multi-word statuses) + if [[ "$status_line" =~ ^status:[[:space:]]*\"([^\"]+)\" ]]; then + new_status="${BASH_REMATCH[1]}" + elif [[ "$status_line" =~ ^status:[[:space:]]*\'([^\']+)\' ]]; then + new_status="${BASH_REMATCH[1]}" + elif [[ "$status_line" =~ ^status:[[:space:]]*(.+)$ ]]; then + # For unquoted values, capture everything after "status:" until end of line + new_status=$(echo "${BASH_REMATCH[1]}" | sed 's/[[:space:]]*$//') + fi + fi + + if [[ "$new_status" == "done" ]]; then + # Get the changed files and commit message + changed_files_array=() + while IFS= read -r line; do + [[ -n "$line" ]] && changed_files_array+=("$line") + done < <(yq -r '.changed_files[]?' "$file_path" 2>/dev/null || true) + + commit_message=$(yq -r '.commit_message // ""' "$file_path" 2>/dev/null || echo "") + + # Remove quotes from commit message if present + if [[ "$commit_message" =~ ^[\"\'](.*)[\"\']$ ]]; then + commit_message="${BASH_REMATCH[1]}" + fi + + # Only proceed if we have both changed files and a commit message + if [[ ${#changed_files_array[@]} -gt 0 ]] && [[ -n "$commit_message" ]]; then + # Stage each changed file + for file in "${changed_files_array[@]}"; do + if [[ -n "$file" ]] && [[ -f "$file" ]]; then + if ! git add "$file" 2>&1 >&2; then + output=$(cat </dev/null; then + if ! git commit -m "$commit_message" > >(cat >&2) 2>&1; then + output=$(cat < /dev/null; then + missing_deps+=("yq") +fi + +# Check for node (required for json npm package) +if ! command -v node &> /dev/null; then + missing_deps+=("node") +fi + +# Check for npx (required to run json package) +if ! command -v npx &> /dev/null; then + missing_deps+=("npx") +fi + +# If there are missing dependencies, display installation instructions +if [ ${#missing_deps[@]} -gt 0 ]; then + message="âš ī¸ Missing required dependencies for rgw workflow hooks:\\n\\n" + + for dep in "${missing_deps[@]}"; do + case "$dep" in + yq) + message+=" đŸ“Ļ yq (YAML processor)\\n" + message+=" macOS: brew install yq\\n" + message+=" Linux: https://github.com/mikefarah/yq#install\\n" + message+="\\n" + ;; + node) + message+=" đŸ“Ļ Node.js (JavaScript runtime)\\n" + message+=" macOS: brew install node\\n" + message+=" Linux: https://nodejs.org/en/download/package-manager\\n" + message+="\\n" + ;; + npx) + message+=" đŸ“Ļ npx (npm package runner)\\n" + message+=" Usually installed with Node.js\\n" + message+=" If missing: npm install -g npx\\n" + message+="\\n" + ;; + esac + done + + message+=" â„šī¸ Install the missing dependencies to enable full hook functionality." + + output=$(cat <