@codyswann/lisa 1.76.5 → 1.77.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.
- package/all/deletions.json +5 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +21 -1
- package/plugins/lisa/hooks/inject-rules.sh +22 -0
- package/{all/copy-overwrite/.claude → plugins/lisa}/rules/base-rules.md +4 -3
- package/{all/copy-overwrite/.claude → plugins/lisa}/rules/verification.md +10 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +23 -1
- package/plugins/lisa-rails/hooks/inject-rules.sh +22 -0
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/hooks/lint-on-edit.sh +3 -1
- package/plugins/src/base/.claude-plugin/plugin.json +4 -0
- package/plugins/src/base/hooks/inject-rules.sh +22 -0
- package/plugins/src/base/rules/base-rules.md +89 -0
- package/plugins/src/base/rules/coding-philosophy.md +428 -0
- package/plugins/src/base/rules/intent-routing.md +126 -0
- package/plugins/src/base/rules/security-audit-handling.md +30 -0
- package/plugins/src/base/rules/verification.md +93 -0
- package/plugins/src/rails/.claude-plugin/plugin.json +6 -0
- package/plugins/src/rails/hooks/inject-rules.sh +22 -0
- package/plugins/src/rails/rules/rails-conventions.md +176 -0
- package/plugins/src/typescript/hooks/lint-on-edit.sh +3 -1
- package/rails/deletions.json +2 -1
- package/plugins/lisa/hooks/enforce-plan-rules.sh +0 -15
- package/plugins/lisa/hooks/sync-tasks.sh +0 -107
- package/plugins/src/base/hooks/enforce-plan-rules.sh +0 -15
- package/plugins/src/base/hooks/sync-tasks.sh +0 -107
- /package/{all/copy-overwrite/.claude → plugins/lisa}/rules/coding-philosophy.md +0 -0
- /package/{all/copy-overwrite/.claude → plugins/lisa}/rules/intent-routing.md +0 -0
- /package/{all/copy-overwrite/.claude → plugins/lisa}/rules/security-audit-handling.md +0 -0
- /package/{rails/copy-overwrite/.claude → plugins/lisa-rails}/rules/rails-conventions.md +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reads all .md files from the plugin's rules/ directory and injects them
|
|
3
|
+
# into the session context via additionalContext.
|
|
4
|
+
# Used by SessionStart and SubagentStart hooks.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
RULES_DIR="${CLAUDE_PLUGIN_ROOT}/rules"
|
|
8
|
+
|
|
9
|
+
# Bail silently if no rules directory
|
|
10
|
+
[ -d "$RULES_DIR" ] || exit 0
|
|
11
|
+
|
|
12
|
+
CONTEXT=""
|
|
13
|
+
for file in "$RULES_DIR"/*.md; do
|
|
14
|
+
[ -f "$file" ] || continue
|
|
15
|
+
CONTEXT+="$(cat "$file")"$'\n\n'
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
# Bail if no rules found
|
|
19
|
+
[ -n "$CONTEXT" ] || exit 0
|
|
20
|
+
|
|
21
|
+
# Output as JSON — jq handles escaping
|
|
22
|
+
jq -n --arg ctx "$CONTEXT" '{"additionalContext": $ctx}'
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Rails Coding Conventions
|
|
2
|
+
|
|
3
|
+
This rule enforces Rails-specific coding standards for consistency, maintainability, and performance.
|
|
4
|
+
|
|
5
|
+
## Fat Models, Skinny Controllers
|
|
6
|
+
|
|
7
|
+
Controllers handle HTTP concerns only. Business logic belongs in models or service objects.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Correct — controller delegates to model
|
|
11
|
+
class OrdersController < ApplicationController
|
|
12
|
+
def create
|
|
13
|
+
@order = Order.place(order_params, current_user)
|
|
14
|
+
redirect_to @order
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Wrong — business logic in controller
|
|
19
|
+
class OrdersController < ApplicationController
|
|
20
|
+
def create
|
|
21
|
+
@order = Order.new(order_params)
|
|
22
|
+
@order.user = current_user
|
|
23
|
+
@order.total = @order.line_items.sum(&:price)
|
|
24
|
+
@order.apply_discount(current_user.discount_rate)
|
|
25
|
+
@order.save!
|
|
26
|
+
OrderMailer.confirmation(@order).deliver_later
|
|
27
|
+
redirect_to @order
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Service Objects
|
|
33
|
+
|
|
34
|
+
Extract complex business logic into service objects when a model method would be too large or spans multiple models.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# app/services/order_placement_service.rb
|
|
38
|
+
class OrderPlacementService
|
|
39
|
+
def initialize(user:, params:)
|
|
40
|
+
@user = user
|
|
41
|
+
@params = params
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call
|
|
45
|
+
order = Order.new(@params)
|
|
46
|
+
order.user = @user
|
|
47
|
+
order.calculate_total
|
|
48
|
+
order.save!
|
|
49
|
+
OrderMailer.confirmation(order).deliver_later
|
|
50
|
+
order
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Concerns
|
|
56
|
+
|
|
57
|
+
Use concerns to share behavior across models or controllers. Keep concerns focused on a single responsibility.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# app/models/concerns/searchable.rb
|
|
61
|
+
module Searchable
|
|
62
|
+
extend ActiveSupport::Concern
|
|
63
|
+
|
|
64
|
+
included do
|
|
65
|
+
scope :search, ->(query) { where("name ILIKE ?", "%#{query}%") }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## ActiveRecord Patterns
|
|
71
|
+
|
|
72
|
+
### Scopes over class methods for chainable queries
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Correct — scope
|
|
76
|
+
scope :active, -> { where(active: true) }
|
|
77
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
78
|
+
|
|
79
|
+
# Wrong — class method for simple query
|
|
80
|
+
def self.active
|
|
81
|
+
where(active: true)
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Validations
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Use built-in validators
|
|
89
|
+
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
90
|
+
validates :age, numericality: { greater_than: 0 }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Callbacks — use sparingly
|
|
94
|
+
|
|
95
|
+
Prefer explicit service objects over callbacks for complex side effects. Callbacks are acceptable for simple data normalization.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# Acceptable — simple normalization
|
|
99
|
+
before_validation :normalize_email
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def normalize_email
|
|
104
|
+
self.email = email&.downcase&.strip
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## N+1 Query Prevention
|
|
109
|
+
|
|
110
|
+
Always use `includes`, `preload`, or `eager_load` to prevent N+1 queries. The Bullet gem is included to detect these in development.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# Correct — eager loading
|
|
114
|
+
@posts = Post.includes(:author, :comments).where(published: true)
|
|
115
|
+
|
|
116
|
+
# Wrong — N+1 query
|
|
117
|
+
@posts = Post.where(published: true)
|
|
118
|
+
@posts.each { |post| post.author.name } # N+1!
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Strong Parameters
|
|
122
|
+
|
|
123
|
+
Always use strong parameters in controllers. Never use `permit!`.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Correct
|
|
127
|
+
def order_params
|
|
128
|
+
params.require(:order).permit(:product_id, :quantity, :notes)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Wrong — permits everything
|
|
132
|
+
def order_params
|
|
133
|
+
params.require(:order).permit!
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Database Migrations
|
|
138
|
+
|
|
139
|
+
- Use `strong_migrations` gem constraints (included via Gemfile.lisa)
|
|
140
|
+
- Never modify `db/schema.rb` directly
|
|
141
|
+
- Always add indexes for foreign keys and commonly queried columns
|
|
142
|
+
- Use `change` method when the migration is reversible; use `up`/`down` when it is not
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class AddIndexToOrdersUserId < ActiveRecord::Migration[7.2]
|
|
146
|
+
def change
|
|
147
|
+
add_index :orders, :user_id
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Testing with RSpec
|
|
153
|
+
|
|
154
|
+
- Use `let` and `let!` for test setup
|
|
155
|
+
- Use `described_class` instead of repeating the class name
|
|
156
|
+
- Use `factory_bot` for test data, not fixtures
|
|
157
|
+
- Use `shoulda-matchers` for model validation tests
|
|
158
|
+
- Keep tests focused — one assertion concept per example
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
RSpec.describe Order, type: :model do
|
|
162
|
+
describe "validations" do
|
|
163
|
+
it { is_expected.to validate_presence_of(:user) }
|
|
164
|
+
it { is_expected.to validate_numericality_of(:total).is_greater_than(0) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe ".recent" do
|
|
168
|
+
it "returns orders in descending creation order" do
|
|
169
|
+
old_order = create(:order, created_at: 1.day.ago)
|
|
170
|
+
new_order = create(:order, created_at: 1.hour.ago)
|
|
171
|
+
|
|
172
|
+
expect(described_class.recent).to eq([new_order, old_order])
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
```
|
|
@@ -55,10 +55,12 @@ fi
|
|
|
55
55
|
# Run ESLint with --fix --quiet --cache on the specific file
|
|
56
56
|
# --quiet: suppress warnings, only show errors
|
|
57
57
|
# --cache: use ESLint cache for performance
|
|
58
|
+
# --rule: disable no-unused-vars auto-fix to prevent removing imports that Claude
|
|
59
|
+
# plans to use in a subsequent edit (pre-commit hook still catches them)
|
|
58
60
|
echo "Running ESLint --fix on: $FILE_PATH"
|
|
59
61
|
|
|
60
62
|
# First pass: attempt auto-fix
|
|
61
|
-
OUTPUT=$($PKG_MANAGER eslint --fix --quiet --cache "$FILE_PATH" 2>&1)
|
|
63
|
+
OUTPUT=$($PKG_MANAGER eslint --fix --quiet --cache --rule '@typescript-eslint/no-unused-vars: off' "$FILE_PATH" 2>&1)
|
|
62
64
|
FIX_EXIT=$?
|
|
63
65
|
|
|
64
66
|
if [ $FIX_EXIT -eq 0 ]; then
|
package/rails/deletions.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
".claude/skills/plan-fix-linter-error",
|
|
9
9
|
".claude/skills/plan-lower-code-complexity",
|
|
10
10
|
".claude/skills/plan-reduce-max-lines",
|
|
11
|
-
".claude/skills/plan-reduce-max-lines-per-function"
|
|
11
|
+
".claude/skills/plan-reduce-max-lines-per-function",
|
|
12
|
+
".claude/rules/rails-conventions.md"
|
|
12
13
|
]
|
|
13
14
|
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Reinjects plan-mode rules on every prompt when Claude is in plan mode.
|
|
3
|
-
# Wired as a UserPromptSubmit hook in .claude/settings.json.
|
|
4
|
-
|
|
5
|
-
INPUT=$(cat)
|
|
6
|
-
PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // "default"')
|
|
7
|
-
|
|
8
|
-
if [ "$PERMISSION_MODE" = "plan" ]; then
|
|
9
|
-
PLAN_RULES="$CLAUDE_PROJECT_DIR/.claude/rules/plan.md"
|
|
10
|
-
if [ -f "$PLAN_RULES" ]; then
|
|
11
|
-
echo "PLAN MODE RULES (reinforced):"
|
|
12
|
-
cat "$PLAN_RULES"
|
|
13
|
-
fi
|
|
14
|
-
fi
|
|
15
|
-
exit 0
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# sync-tasks.sh - Syncs Claude Code tasks to project directories
|
|
4
|
-
#
|
|
5
|
-
# This hook is triggered on PostToolUse for TaskCreate and TaskUpdate.
|
|
6
|
-
# It reads the task metadata to determine the project and syncs
|
|
7
|
-
# task JSON files to ./projects/{project}/tasks/{session-id}/
|
|
8
|
-
#
|
|
9
|
-
# This session-based structure preserves task history across /clear commands,
|
|
10
|
-
# preventing overwrites when new sessions create tasks with the same IDs.
|
|
11
|
-
#
|
|
12
|
-
# Input (via stdin): JSON with tool_name, tool_input, tool_response
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
# Temporarily disable this hook
|
|
16
|
-
exit 0
|
|
17
|
-
|
|
18
|
-
set -euo pipefail
|
|
19
|
-
|
|
20
|
-
# Read JSON input from stdin
|
|
21
|
-
INPUT=$(cat)
|
|
22
|
-
|
|
23
|
-
# Extract tool name
|
|
24
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
25
|
-
|
|
26
|
-
# Only process TaskCreate and TaskUpdate
|
|
27
|
-
if [[ "$TOOL_NAME" != "TaskCreate" && "$TOOL_NAME" != "TaskUpdate" ]]; then
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# Try to get project from multiple sources:
|
|
32
|
-
# 1. Task metadata (passed in tool_input)
|
|
33
|
-
# 2. .claude-active-project marker file
|
|
34
|
-
|
|
35
|
-
PROJECT=""
|
|
36
|
-
|
|
37
|
-
# Check tool_input metadata for project
|
|
38
|
-
PROJECT=$(echo "$INPUT" | jq -r '.tool_input.metadata.project // empty')
|
|
39
|
-
|
|
40
|
-
# If no project in metadata, check marker file
|
|
41
|
-
if [[ -z "$PROJECT" && -f ".claude-active-project" ]]; then
|
|
42
|
-
PROJECT=$(cat .claude-active-project | tr -d '[:space:]')
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
# If still no project, skip syncing
|
|
46
|
-
if [[ -z "$PROJECT" ]]; then
|
|
47
|
-
exit 0
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Validate project name (kebab-case, no path traversal)
|
|
51
|
-
if [[ ! "$PROJECT" =~ ^[a-z0-9-]+$ ]]; then
|
|
52
|
-
echo "Warning: Invalid project name '$PROJECT', skipping sync" >&2
|
|
53
|
-
exit 0
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
# Get task ID
|
|
57
|
-
TASK_ID=""
|
|
58
|
-
if [[ "$TOOL_NAME" == "TaskCreate" ]]; then
|
|
59
|
-
# For TaskCreate, ID is in tool_response.task.id
|
|
60
|
-
TASK_ID=$(echo "$INPUT" | jq -r '.tool_response.task.id // empty')
|
|
61
|
-
elif [[ "$TOOL_NAME" == "TaskUpdate" ]]; then
|
|
62
|
-
# For TaskUpdate, ID is in tool_input
|
|
63
|
-
TASK_ID=$(echo "$INPUT" | jq -r '.tool_input.taskId // empty')
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
if [[ -z "$TASK_ID" ]]; then
|
|
67
|
-
exit 0
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# Find the task file in ~/.claude/tasks/
|
|
71
|
-
# Tasks are stored in ~/.claude/tasks/{session-uuid}/{id}.json
|
|
72
|
-
CLAUDE_TASKS_DIR="${HOME}/.claude/tasks"
|
|
73
|
-
TASK_FILE=""
|
|
74
|
-
|
|
75
|
-
# Get session ID from hook input (preferred - 100% accurate)
|
|
76
|
-
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
77
|
-
|
|
78
|
-
if [[ -n "$SESSION_ID" && -f "${CLAUDE_TASKS_DIR}/${SESSION_ID}/${TASK_ID}.json" ]]; then
|
|
79
|
-
# Use session ID directly - guaranteed correct session
|
|
80
|
-
TASK_FILE="${CLAUDE_TASKS_DIR}/${SESSION_ID}/${TASK_ID}.json"
|
|
81
|
-
else
|
|
82
|
-
# Fallback: find most recently modified task file with this ID
|
|
83
|
-
# This handles edge cases where session_id isn't available
|
|
84
|
-
TASK_FILE=$(find "$CLAUDE_TASKS_DIR" -name "${TASK_ID}.json" -exec stat -f '%m %N' {} \; 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
if [[ -z "$TASK_FILE" || ! -f "$TASK_FILE" ]]; then
|
|
88
|
-
exit 0
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
# Require session ID for proper history tracking
|
|
92
|
-
if [[ -z "$SESSION_ID" ]]; then
|
|
93
|
-
echo "Warning: No session_id available, skipping sync" >&2
|
|
94
|
-
exit 0
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
# Ensure project tasks directory exists (includes session ID for history preservation)
|
|
98
|
-
PROJECT_TASKS_DIR="./projects/${PROJECT}/tasks/${SESSION_ID}"
|
|
99
|
-
mkdir -p "$PROJECT_TASKS_DIR"
|
|
100
|
-
|
|
101
|
-
# Copy task file to project directory
|
|
102
|
-
cp "$TASK_FILE" "${PROJECT_TASKS_DIR}/${TASK_ID}.json"
|
|
103
|
-
|
|
104
|
-
# Optionally stage the file for git (non-blocking)
|
|
105
|
-
git add "${PROJECT_TASKS_DIR}/${TASK_ID}.json" 2>/dev/null || true
|
|
106
|
-
|
|
107
|
-
exit 0
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Reinjects plan-mode rules on every prompt when Claude is in plan mode.
|
|
3
|
-
# Wired as a UserPromptSubmit hook in .claude/settings.json.
|
|
4
|
-
|
|
5
|
-
INPUT=$(cat)
|
|
6
|
-
PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // "default"')
|
|
7
|
-
|
|
8
|
-
if [ "$PERMISSION_MODE" = "plan" ]; then
|
|
9
|
-
PLAN_RULES="$CLAUDE_PROJECT_DIR/.claude/rules/plan.md"
|
|
10
|
-
if [ -f "$PLAN_RULES" ]; then
|
|
11
|
-
echo "PLAN MODE RULES (reinforced):"
|
|
12
|
-
cat "$PLAN_RULES"
|
|
13
|
-
fi
|
|
14
|
-
fi
|
|
15
|
-
exit 0
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# sync-tasks.sh - Syncs Claude Code tasks to project directories
|
|
4
|
-
#
|
|
5
|
-
# This hook is triggered on PostToolUse for TaskCreate and TaskUpdate.
|
|
6
|
-
# It reads the task metadata to determine the project and syncs
|
|
7
|
-
# task JSON files to ./projects/{project}/tasks/{session-id}/
|
|
8
|
-
#
|
|
9
|
-
# This session-based structure preserves task history across /clear commands,
|
|
10
|
-
# preventing overwrites when new sessions create tasks with the same IDs.
|
|
11
|
-
#
|
|
12
|
-
# Input (via stdin): JSON with tool_name, tool_input, tool_response
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
# Temporarily disable this hook
|
|
16
|
-
exit 0
|
|
17
|
-
|
|
18
|
-
set -euo pipefail
|
|
19
|
-
|
|
20
|
-
# Read JSON input from stdin
|
|
21
|
-
INPUT=$(cat)
|
|
22
|
-
|
|
23
|
-
# Extract tool name
|
|
24
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
25
|
-
|
|
26
|
-
# Only process TaskCreate and TaskUpdate
|
|
27
|
-
if [[ "$TOOL_NAME" != "TaskCreate" && "$TOOL_NAME" != "TaskUpdate" ]]; then
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# Try to get project from multiple sources:
|
|
32
|
-
# 1. Task metadata (passed in tool_input)
|
|
33
|
-
# 2. .claude-active-project marker file
|
|
34
|
-
|
|
35
|
-
PROJECT=""
|
|
36
|
-
|
|
37
|
-
# Check tool_input metadata for project
|
|
38
|
-
PROJECT=$(echo "$INPUT" | jq -r '.tool_input.metadata.project // empty')
|
|
39
|
-
|
|
40
|
-
# If no project in metadata, check marker file
|
|
41
|
-
if [[ -z "$PROJECT" && -f ".claude-active-project" ]]; then
|
|
42
|
-
PROJECT=$(cat .claude-active-project | tr -d '[:space:]')
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
# If still no project, skip syncing
|
|
46
|
-
if [[ -z "$PROJECT" ]]; then
|
|
47
|
-
exit 0
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Validate project name (kebab-case, no path traversal)
|
|
51
|
-
if [[ ! "$PROJECT" =~ ^[a-z0-9-]+$ ]]; then
|
|
52
|
-
echo "Warning: Invalid project name '$PROJECT', skipping sync" >&2
|
|
53
|
-
exit 0
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
# Get task ID
|
|
57
|
-
TASK_ID=""
|
|
58
|
-
if [[ "$TOOL_NAME" == "TaskCreate" ]]; then
|
|
59
|
-
# For TaskCreate, ID is in tool_response.task.id
|
|
60
|
-
TASK_ID=$(echo "$INPUT" | jq -r '.tool_response.task.id // empty')
|
|
61
|
-
elif [[ "$TOOL_NAME" == "TaskUpdate" ]]; then
|
|
62
|
-
# For TaskUpdate, ID is in tool_input
|
|
63
|
-
TASK_ID=$(echo "$INPUT" | jq -r '.tool_input.taskId // empty')
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
if [[ -z "$TASK_ID" ]]; then
|
|
67
|
-
exit 0
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# Find the task file in ~/.claude/tasks/
|
|
71
|
-
# Tasks are stored in ~/.claude/tasks/{session-uuid}/{id}.json
|
|
72
|
-
CLAUDE_TASKS_DIR="${HOME}/.claude/tasks"
|
|
73
|
-
TASK_FILE=""
|
|
74
|
-
|
|
75
|
-
# Get session ID from hook input (preferred - 100% accurate)
|
|
76
|
-
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
77
|
-
|
|
78
|
-
if [[ -n "$SESSION_ID" && -f "${CLAUDE_TASKS_DIR}/${SESSION_ID}/${TASK_ID}.json" ]]; then
|
|
79
|
-
# Use session ID directly - guaranteed correct session
|
|
80
|
-
TASK_FILE="${CLAUDE_TASKS_DIR}/${SESSION_ID}/${TASK_ID}.json"
|
|
81
|
-
else
|
|
82
|
-
# Fallback: find most recently modified task file with this ID
|
|
83
|
-
# This handles edge cases where session_id isn't available
|
|
84
|
-
TASK_FILE=$(find "$CLAUDE_TASKS_DIR" -name "${TASK_ID}.json" -exec stat -f '%m %N' {} \; 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
if [[ -z "$TASK_FILE" || ! -f "$TASK_FILE" ]]; then
|
|
88
|
-
exit 0
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
# Require session ID for proper history tracking
|
|
92
|
-
if [[ -z "$SESSION_ID" ]]; then
|
|
93
|
-
echo "Warning: No session_id available, skipping sync" >&2
|
|
94
|
-
exit 0
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
# Ensure project tasks directory exists (includes session ID for history preservation)
|
|
98
|
-
PROJECT_TASKS_DIR="./projects/${PROJECT}/tasks/${SESSION_ID}"
|
|
99
|
-
mkdir -p "$PROJECT_TASKS_DIR"
|
|
100
|
-
|
|
101
|
-
# Copy task file to project directory
|
|
102
|
-
cp "$TASK_FILE" "${PROJECT_TASKS_DIR}/${TASK_ID}.json"
|
|
103
|
-
|
|
104
|
-
# Optionally stage the file for git (non-blocking)
|
|
105
|
-
git add "${PROJECT_TASKS_DIR}/${TASK_ID}.json" 2>/dev/null || true
|
|
106
|
-
|
|
107
|
-
exit 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|