start-vibing 1.1.1
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/README.md +149 -0
- package/dist/cli.js +199 -0
- package/package.json +42 -0
- package/template/.claude/CLAUDE.md +168 -0
- package/template/.claude/README.md +208 -0
- package/template/.claude/agents/analyzer.md +139 -0
- package/template/.claude/agents/commit-manager.md +231 -0
- package/template/.claude/agents/documenter.md +160 -0
- package/template/.claude/agents/domain-updater.md +200 -0
- package/template/.claude/agents/final-validator.md +182 -0
- package/template/.claude/agents/orchestrator.md +136 -0
- package/template/.claude/agents/quality-checker.md +264 -0
- package/template/.claude/agents/research.md +262 -0
- package/template/.claude/agents/security-auditor.md +199 -0
- package/template/.claude/agents/tester.md +572 -0
- package/template/.claude/agents/ui-ux-reviewer.md +180 -0
- package/template/.claude/commands/feature.md +102 -0
- package/template/.claude/commands/fix.md +80 -0
- package/template/.claude/commands/research.md +107 -0
- package/template/.claude/commands/validate.md +72 -0
- package/template/.claude/config/README.md +30 -0
- package/template/.claude/config/domain-mapping.json +26 -0
- package/template/.claude/config/project-config.json +53 -0
- package/template/.claude/config/quality-gates.json +46 -0
- package/template/.claude/config/security-rules.json +45 -0
- package/template/.claude/config/testing-config.json +168 -0
- package/template/.claude/hooks/SETUP.md +181 -0
- package/template/.claude/hooks/post-tool-use.py +155 -0
- package/template/.claude/hooks/pre-tool-use.py +159 -0
- package/template/.claude/hooks/security-check.js +202 -0
- package/template/.claude/hooks/stop-validation.py +155 -0
- package/template/.claude/hooks/user-prompt-submit.py +277 -0
- package/template/.claude/hooks/validate-commit.py +200 -0
- package/template/.claude/hooks/workflow-manager.py +350 -0
- package/template/.claude/settings.json +269 -0
- package/template/.claude/skills/codebase-knowledge/SKILL.md +145 -0
- package/template/.claude/skills/codebase-knowledge/TEMPLATE.md +35 -0
- package/template/.claude/skills/codebase-knowledge/domains/claude-system.md +321 -0
- package/template/.claude/skills/docs-tracker/SKILL.md +239 -0
- package/template/.claude/skills/final-check/SKILL.md +284 -0
- package/template/.claude/skills/quality-gate/SKILL.md +278 -0
- package/template/.claude/skills/research-cache/SKILL.md +207 -0
- package/template/.claude/skills/security-scan/SKILL.md +206 -0
- package/template/.claude/skills/test-coverage/SKILL.md +441 -0
- package/template/.claude/skills/ui-ux-audit/SKILL.md +254 -0
- package/template/.claude/workflow-state.schema.json +200 -0
- package/template/CLAUDE.md +96 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Security rules. Used by security-auditor agent.",
|
|
3
|
+
|
|
4
|
+
"authentication": {
|
|
5
|
+
"framework": "session-based",
|
|
6
|
+
"userIdSource": "ctx.user._id",
|
|
7
|
+
"protectedProcedure": "protectedProcedure",
|
|
8
|
+
"sessionStore": "mongodb"
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
"validation": {
|
|
12
|
+
"library": "zod",
|
|
13
|
+
"requireOnAllRoutes": true
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"sensitivePatterns": {
|
|
17
|
+
"forbidden": [
|
|
18
|
+
"input.userId",
|
|
19
|
+
"input.user_id",
|
|
20
|
+
"req.body.userId",
|
|
21
|
+
"passwordHash",
|
|
22
|
+
"password:"
|
|
23
|
+
],
|
|
24
|
+
"files": ["auth/", "api/", "server/", "routers/"]
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
"cryptography": {
|
|
28
|
+
"passwordHashing": "bcrypt",
|
|
29
|
+
"minSaltRounds": 10,
|
|
30
|
+
"tokenGeneration": "crypto.randomBytes"
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
"cookies": {
|
|
34
|
+
"httpOnly": true,
|
|
35
|
+
"secure": true,
|
|
36
|
+
"sameSite": "strict"
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"owaspChecks": {
|
|
40
|
+
"a01_brokenAccessControl": true,
|
|
41
|
+
"a02_cryptographicFailures": true,
|
|
42
|
+
"a03_injection": true,
|
|
43
|
+
"a07_authenticationFailures": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Testing configuration. Used by tester agent.",
|
|
3
|
+
|
|
4
|
+
"framework": {
|
|
5
|
+
"unit": "vitest",
|
|
6
|
+
"e2e": "playwright",
|
|
7
|
+
"version": "1.40+"
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
"paths": {
|
|
11
|
+
"unitTests": "tests/unit/*.test.ts",
|
|
12
|
+
"e2eTests": "tests/e2e/**/*.spec.ts",
|
|
13
|
+
"fixtures": "tests/e2e/fixtures/",
|
|
14
|
+
"pages": "tests/e2e/pages/",
|
|
15
|
+
"flows": "tests/e2e/flows/",
|
|
16
|
+
"api": "tests/e2e/api/",
|
|
17
|
+
"authState": "tests/e2e/.auth/"
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
"viewports": {
|
|
21
|
+
"desktop": {
|
|
22
|
+
"device": "Desktop Chrome",
|
|
23
|
+
"width": 1280,
|
|
24
|
+
"height": 800,
|
|
25
|
+
"required": true
|
|
26
|
+
},
|
|
27
|
+
"tablet": {
|
|
28
|
+
"device": "iPad",
|
|
29
|
+
"width": 768,
|
|
30
|
+
"height": 1024,
|
|
31
|
+
"required": true
|
|
32
|
+
},
|
|
33
|
+
"mobile": {
|
|
34
|
+
"device": "iPhone SE",
|
|
35
|
+
"width": 375,
|
|
36
|
+
"height": 667,
|
|
37
|
+
"required": true
|
|
38
|
+
},
|
|
39
|
+
"mobileLarge": {
|
|
40
|
+
"device": "iPhone 14",
|
|
41
|
+
"width": 390,
|
|
42
|
+
"height": 844,
|
|
43
|
+
"required": false
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
"auth": {
|
|
48
|
+
"storageStatePath": "tests/e2e/.auth/user.json",
|
|
49
|
+
"loginPage": "/auth/login",
|
|
50
|
+
"registerPage": "/auth/register",
|
|
51
|
+
"protectedPrefix": "/app",
|
|
52
|
+
"setupProject": "setup",
|
|
53
|
+
"reuseSession": true
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
"database": {
|
|
57
|
+
"type": "mongodb",
|
|
58
|
+
"testConnectionEnv": "MONGODB_URI",
|
|
59
|
+
"cleanupStrategy": "fixture-tracking",
|
|
60
|
+
"verifyAfterActions": true
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"cleanup": {
|
|
64
|
+
"strategy": "fixture-based",
|
|
65
|
+
"trackCreatedIds": true,
|
|
66
|
+
"deleteOnlyTracked": true,
|
|
67
|
+
"cleanupOnFailure": true,
|
|
68
|
+
"collections": ["users", "items", "sessions"]
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
"dataTestIds": {
|
|
72
|
+
"form": {
|
|
73
|
+
"nameInput": "name-input",
|
|
74
|
+
"emailInput": "email-input",
|
|
75
|
+
"passwordInput": "password-input",
|
|
76
|
+
"confirmPasswordInput": "confirm-password-input",
|
|
77
|
+
"submitButton": "submit-button"
|
|
78
|
+
},
|
|
79
|
+
"feedback": {
|
|
80
|
+
"errorMessage": "error-message",
|
|
81
|
+
"successMessage": "success-message",
|
|
82
|
+
"loadingSpinner": "loading-spinner"
|
|
83
|
+
},
|
|
84
|
+
"navigation": {
|
|
85
|
+
"sidebar": "sidebar",
|
|
86
|
+
"hamburgerMenu": "hamburger-menu",
|
|
87
|
+
"mobileNav": "mobile-nav",
|
|
88
|
+
"logoutButton": "logout-button"
|
|
89
|
+
},
|
|
90
|
+
"actions": {
|
|
91
|
+
"deleteButton": "delete-button",
|
|
92
|
+
"editButton": "edit-button",
|
|
93
|
+
"confirmDelete": "confirm-delete",
|
|
94
|
+
"cancelButton": "cancel-button"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
"api": {
|
|
99
|
+
"rest": {
|
|
100
|
+
"baseUrl": "/api",
|
|
101
|
+
"authEndpoint": "/api/auth/login",
|
|
102
|
+
"validateInput": true,
|
|
103
|
+
"requireAuth": true
|
|
104
|
+
},
|
|
105
|
+
"trpc": {
|
|
106
|
+
"baseUrl": "/api/trpc",
|
|
107
|
+
"batchEnabled": true,
|
|
108
|
+
"validateInput": true
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
"security": {
|
|
113
|
+
"testForbiddenRequests": true,
|
|
114
|
+
"testRateLimiting": true,
|
|
115
|
+
"testCrossUserAccess": true,
|
|
116
|
+
"testUnauthenticated": true,
|
|
117
|
+
"expectedForbiddenStatus": 403,
|
|
118
|
+
"expectedUnauthorizedStatus": 401,
|
|
119
|
+
"expectedRateLimitStatus": 429
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
"flows": {
|
|
123
|
+
"required": [
|
|
124
|
+
"registration",
|
|
125
|
+
"login",
|
|
126
|
+
"logout",
|
|
127
|
+
"crud-create",
|
|
128
|
+
"crud-read",
|
|
129
|
+
"crud-update",
|
|
130
|
+
"crud-delete",
|
|
131
|
+
"permissions"
|
|
132
|
+
],
|
|
133
|
+
"optional": [
|
|
134
|
+
"password-reset",
|
|
135
|
+
"email-verification",
|
|
136
|
+
"profile-update"
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
"commands": {
|
|
141
|
+
"install": "bun add -D @playwright/test && bunx playwright install",
|
|
142
|
+
"run": "bunx playwright test",
|
|
143
|
+
"runUi": "bunx playwright test --ui",
|
|
144
|
+
"runHeaded": "bunx playwright test --headed",
|
|
145
|
+
"runMobile": "bunx playwright test --project='iPhone SE'",
|
|
146
|
+
"debug": "bunx playwright test --debug",
|
|
147
|
+
"report": "bunx playwright show-report",
|
|
148
|
+
"codegen": "bunx playwright codegen"
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
"rules": {
|
|
152
|
+
"noSkip": true,
|
|
153
|
+
"noMockAuth": true,
|
|
154
|
+
"requireCleanup": true,
|
|
155
|
+
"requireDbValidation": true,
|
|
156
|
+
"requireViewportTests": true,
|
|
157
|
+
"requireDataTestId": true,
|
|
158
|
+
"uniqueTestData": true,
|
|
159
|
+
"timestampEmails": true
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
"reporting": {
|
|
163
|
+
"trace": "on-first-retry",
|
|
164
|
+
"screenshot": "only-on-failure",
|
|
165
|
+
"video": "retain-on-failure",
|
|
166
|
+
"outputFolder": "test-results"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Workflow Enforcement Hooks - Setup Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This system enforces the agent workflow by:
|
|
6
|
+
|
|
7
|
+
1. **Blocking file modifications** until workflow is started and files are approved
|
|
8
|
+
2. **Tracking all changes** automatically
|
|
9
|
+
3. **Preventing incomplete workflow** from stopping
|
|
10
|
+
4. **Blocking commits** that don't follow the workflow
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Python 3.8+
|
|
15
|
+
- Git
|
|
16
|
+
- Husky (for pre-commit hooks)
|
|
17
|
+
|
|
18
|
+
## Files
|
|
19
|
+
|
|
20
|
+
| File | Purpose |
|
|
21
|
+
| --------------------- | ------------------------------------------------ |
|
|
22
|
+
| `pre-tool-use.py` | Blocks Edit/Write unless file is approved |
|
|
23
|
+
| `post-tool-use.py` | Auto-tracks modifications to workflow-state.json |
|
|
24
|
+
| `stop-validation.py` | Blocks stopping if workflow incomplete |
|
|
25
|
+
| `validate-commit.py` | Husky pre-commit validation |
|
|
26
|
+
| `workflow-manager.py` | CLI for agents to update workflow state |
|
|
27
|
+
|
|
28
|
+
## Husky Integration
|
|
29
|
+
|
|
30
|
+
### 1. Install Husky
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun add husky --dev
|
|
34
|
+
bunx husky init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Create Pre-Commit Hook
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Create .husky/pre-commit
|
|
41
|
+
echo '#!/usr/bin/env sh
|
|
42
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
43
|
+
|
|
44
|
+
python .claude/hooks/validate-commit.py
|
|
45
|
+
' > .husky/pre-commit
|
|
46
|
+
|
|
47
|
+
chmod +x .husky/pre-commit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Add to package.json
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"scripts": {
|
|
55
|
+
"prepare": "husky"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Claude Code Configuration
|
|
61
|
+
|
|
62
|
+
The hooks are configured in `.claude/settings.json`:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"hooks": {
|
|
67
|
+
"PreToolUse": [
|
|
68
|
+
{
|
|
69
|
+
"matcher": "Edit|Write|NotebookEdit",
|
|
70
|
+
"hooks": [
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-tool-use.py\"",
|
|
74
|
+
"timeout": 30
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"PostToolUse": [
|
|
80
|
+
{
|
|
81
|
+
"matcher": "Edit|Write|NotebookEdit",
|
|
82
|
+
"hooks": [
|
|
83
|
+
{
|
|
84
|
+
"type": "command",
|
|
85
|
+
"command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-use.py\"",
|
|
86
|
+
"timeout": 30
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"Stop": [
|
|
92
|
+
{
|
|
93
|
+
"matcher": "",
|
|
94
|
+
"hooks": [
|
|
95
|
+
{
|
|
96
|
+
"type": "command",
|
|
97
|
+
"command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/stop-validation.py\"",
|
|
98
|
+
"timeout": 30
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**IMPORTANT:** Use `$CLAUDE_PROJECT_DIR` to ensure hooks work from any directory.
|
|
108
|
+
|
|
109
|
+
## Workflow Manager CLI
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Start a task (REQUIRED FIRST)
|
|
113
|
+
python .claude/hooks/workflow-manager.py start-task --type feature --description "Add user auth"
|
|
114
|
+
|
|
115
|
+
# Approve files for modification
|
|
116
|
+
python .claude/hooks/workflow-manager.py approve-files --files "src/auth.ts" "app/login/*"
|
|
117
|
+
|
|
118
|
+
# Mark agent as executed
|
|
119
|
+
python .claude/hooks/workflow-manager.py agent-executed --agent analyzer --result approved
|
|
120
|
+
|
|
121
|
+
# Record quality gate result
|
|
122
|
+
python .claude/hooks/workflow-manager.py quality-gate --gate typecheck --passed true
|
|
123
|
+
|
|
124
|
+
# Record security audit
|
|
125
|
+
python .claude/hooks/workflow-manager.py security-audit --result approved
|
|
126
|
+
|
|
127
|
+
# Final validation
|
|
128
|
+
python .claude/hooks/workflow-manager.py final-validation --result approved --ready-to-commit true
|
|
129
|
+
|
|
130
|
+
# Complete task after commit
|
|
131
|
+
python .claude/hooks/workflow-manager.py complete-task --commit-hash abc123def
|
|
132
|
+
|
|
133
|
+
# Check current status
|
|
134
|
+
python .claude/hooks/workflow-manager.py status
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Error Messages
|
|
138
|
+
|
|
139
|
+
### "BLOCKED: No active task"
|
|
140
|
+
|
|
141
|
+
Run `workflow-manager.py start-task` first.
|
|
142
|
+
|
|
143
|
+
### "BLOCKED: Analyzer agent has not executed"
|
|
144
|
+
|
|
145
|
+
Run the analyzer agent and call `workflow-manager.py agent-executed --agent analyzer --result approved`.
|
|
146
|
+
|
|
147
|
+
### "BLOCKED: File not in approved list"
|
|
148
|
+
|
|
149
|
+
Add the file to approved list via `workflow-manager.py approve-files --files "path/to/file"`.
|
|
150
|
+
|
|
151
|
+
### "WORKFLOW INCOMPLETE - Cannot stop yet"
|
|
152
|
+
|
|
153
|
+
Execute all required agents before stopping the session.
|
|
154
|
+
|
|
155
|
+
### "COMMIT BLOCKED - Workflow validation failed"
|
|
156
|
+
|
|
157
|
+
Ensure all agents executed, tests created, and final validation approved.
|
|
158
|
+
|
|
159
|
+
## Environment Variables
|
|
160
|
+
|
|
161
|
+
| Variable | Default | Description |
|
|
162
|
+
| -------------------- | ------------- | ---------------------- |
|
|
163
|
+
| `CLAUDE_PROJECT_DIR` | `os.getcwd()` | Project root directory |
|
|
164
|
+
|
|
165
|
+
## Troubleshooting
|
|
166
|
+
|
|
167
|
+
### Hooks not executing
|
|
168
|
+
|
|
169
|
+
1. Verify Python is in PATH
|
|
170
|
+
2. Check `.claude/settings.json` hooks configuration
|
|
171
|
+
3. Ensure hook files have execute permissions
|
|
172
|
+
|
|
173
|
+
### Workflow state corrupted
|
|
174
|
+
|
|
175
|
+
Delete `.claude/workflow-state.json` and start fresh.
|
|
176
|
+
|
|
177
|
+
### Husky not running
|
|
178
|
+
|
|
179
|
+
1. Run `bun run prepare`
|
|
180
|
+
2. Check `.husky/pre-commit` has execute permissions
|
|
181
|
+
3. Verify Git hooks are enabled: `git config core.hooksPath`
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse Hook - Track File Modifications
|
|
4
|
+
|
|
5
|
+
This hook records all file modifications to workflow-state.json
|
|
6
|
+
for later validation by Husky pre-commit hook.
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
- 0: Always (tracking is non-blocking)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Get project directory
|
|
19
|
+
PROJECT_DIR = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
|
|
20
|
+
WORKFLOW_STATE_PATH = Path(PROJECT_DIR) / '.claude' / 'workflow-state.json'
|
|
21
|
+
|
|
22
|
+
# Tools that modify files
|
|
23
|
+
FILE_MODIFY_TOOLS = {'Edit', 'Write', 'NotebookEdit'}
|
|
24
|
+
FILE_DELETE_TOOLS = {'Bash'} # rm commands
|
|
25
|
+
|
|
26
|
+
# Test file patterns
|
|
27
|
+
TEST_PATTERNS = ['test', 'spec', '.test.', '.spec.', '__tests__']
|
|
28
|
+
|
|
29
|
+
# Doc file patterns
|
|
30
|
+
DOC_PATTERNS = ['docs/', 'README', 'CHANGELOG', '.md', 'domains/']
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_workflow_state():
|
|
34
|
+
"""Load current workflow state"""
|
|
35
|
+
if not WORKFLOW_STATE_PATH.exists():
|
|
36
|
+
return {"version": "1.0.0", "currentTask": None, "sessions": []}
|
|
37
|
+
try:
|
|
38
|
+
with open(WORKFLOW_STATE_PATH) as f:
|
|
39
|
+
return json.load(f)
|
|
40
|
+
except (json.JSONDecodeError, IOError):
|
|
41
|
+
return {"version": "1.0.0", "currentTask": None, "sessions": []}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save_workflow_state(state: dict):
|
|
45
|
+
"""Save workflow state"""
|
|
46
|
+
WORKFLOW_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
with open(WORKFLOW_STATE_PATH, 'w') as f:
|
|
48
|
+
json.dump(state, f, indent=2)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_test_file(path: str) -> bool:
|
|
52
|
+
"""Check if file is a test file"""
|
|
53
|
+
return any(p in path.lower() for p in TEST_PATTERNS)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_doc_file(path: str) -> bool:
|
|
57
|
+
"""Check if file is a documentation file"""
|
|
58
|
+
return any(p in path.lower() for p in DOC_PATTERNS)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
# Read hook input from stdin
|
|
63
|
+
try:
|
|
64
|
+
hook_input = json.load(sys.stdin)
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
tool_name = hook_input.get('tool_name', '')
|
|
69
|
+
tool_input = hook_input.get('tool_input', {})
|
|
70
|
+
tool_result = hook_input.get('tool_result', {})
|
|
71
|
+
|
|
72
|
+
# Only track file modification tools
|
|
73
|
+
if tool_name not in FILE_MODIFY_TOOLS:
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
|
|
76
|
+
# Get file path
|
|
77
|
+
file_path = tool_input.get('file_path', tool_input.get('notebook_path', ''))
|
|
78
|
+
|
|
79
|
+
if not file_path:
|
|
80
|
+
sys.exit(0)
|
|
81
|
+
|
|
82
|
+
# Skip workflow state file itself
|
|
83
|
+
if 'workflow-state.json' in file_path:
|
|
84
|
+
sys.exit(0)
|
|
85
|
+
|
|
86
|
+
# Load state
|
|
87
|
+
state = load_workflow_state()
|
|
88
|
+
|
|
89
|
+
# If no current task, just exit (pre-tool-use should have blocked)
|
|
90
|
+
if not state.get('currentTask'):
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
|
|
93
|
+
task = state['currentTask']
|
|
94
|
+
|
|
95
|
+
# Determine action type
|
|
96
|
+
# For Write tool, check if file existed
|
|
97
|
+
action = 'modified'
|
|
98
|
+
if tool_name == 'Write':
|
|
99
|
+
# Assume created if not in modified list
|
|
100
|
+
existing = [m['path'] for m in task.get('modifiedFiles', [])]
|
|
101
|
+
if file_path not in existing:
|
|
102
|
+
action = 'created'
|
|
103
|
+
|
|
104
|
+
# Normalize path
|
|
105
|
+
file_path = file_path.replace('\\', '/')
|
|
106
|
+
|
|
107
|
+
# Check if file was approved
|
|
108
|
+
approved_files = task.get('approvedFiles', [])
|
|
109
|
+
was_approved = any(
|
|
110
|
+
file_path.endswith(af) or file_path == af or
|
|
111
|
+
(af.endswith('*') and file_path.startswith(af[:-1]))
|
|
112
|
+
for af in approved_files
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Record modification
|
|
116
|
+
modification = {
|
|
117
|
+
'path': file_path,
|
|
118
|
+
'action': action,
|
|
119
|
+
'timestamp': datetime.now().isoformat(),
|
|
120
|
+
'approved': was_approved
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if 'modifiedFiles' not in task:
|
|
124
|
+
task['modifiedFiles'] = []
|
|
125
|
+
|
|
126
|
+
# Update or add
|
|
127
|
+
existing_paths = [m['path'] for m in task['modifiedFiles']]
|
|
128
|
+
if file_path in existing_paths:
|
|
129
|
+
idx = existing_paths.index(file_path)
|
|
130
|
+
task['modifiedFiles'][idx] = modification
|
|
131
|
+
else:
|
|
132
|
+
task['modifiedFiles'].append(modification)
|
|
133
|
+
|
|
134
|
+
# Track test files
|
|
135
|
+
if is_test_file(file_path):
|
|
136
|
+
if 'testsCreated' not in task:
|
|
137
|
+
task['testsCreated'] = []
|
|
138
|
+
if file_path not in task['testsCreated']:
|
|
139
|
+
task['testsCreated'].append(file_path)
|
|
140
|
+
|
|
141
|
+
# Track doc files
|
|
142
|
+
if is_doc_file(file_path):
|
|
143
|
+
if 'docsUpdated' not in task:
|
|
144
|
+
task['docsUpdated'] = []
|
|
145
|
+
if file_path not in task['docsUpdated']:
|
|
146
|
+
task['docsUpdated'].append(file_path)
|
|
147
|
+
|
|
148
|
+
# Save state
|
|
149
|
+
save_workflow_state(state)
|
|
150
|
+
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == '__main__':
|
|
155
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse Hook - File Modification Enforcement
|
|
4
|
+
|
|
5
|
+
This hook blocks file modifications unless:
|
|
6
|
+
1. The analyzer agent has approved the file
|
|
7
|
+
2. The file is in the approvedFiles list in workflow-state.json
|
|
8
|
+
|
|
9
|
+
Exit codes:
|
|
10
|
+
- 0: Allow the operation
|
|
11
|
+
- 2: Block the operation (shows stderr to Claude)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import os
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Get project directory
|
|
21
|
+
PROJECT_DIR = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
|
|
22
|
+
WORKFLOW_STATE_PATH = Path(PROJECT_DIR) / '.claude' / 'workflow-state.json'
|
|
23
|
+
|
|
24
|
+
# Tools that modify files
|
|
25
|
+
FILE_MODIFY_TOOLS = {'Edit', 'Write', 'NotebookEdit'}
|
|
26
|
+
|
|
27
|
+
# Files always allowed (config, etc)
|
|
28
|
+
ALWAYS_ALLOWED = {
|
|
29
|
+
'.claude/workflow-state.json',
|
|
30
|
+
'.claude/hooks/',
|
|
31
|
+
'package-lock.json',
|
|
32
|
+
'node_modules/',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Files always blocked
|
|
36
|
+
ALWAYS_BLOCKED = {
|
|
37
|
+
'.env',
|
|
38
|
+
'.env.local',
|
|
39
|
+
'.env.production',
|
|
40
|
+
'credentials.json',
|
|
41
|
+
'secrets/',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_workflow_state():
|
|
46
|
+
"""Load current workflow state"""
|
|
47
|
+
if not WORKFLOW_STATE_PATH.exists():
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
with open(WORKFLOW_STATE_PATH) as f:
|
|
51
|
+
return json.load(f)
|
|
52
|
+
except (json.JSONDecodeError, IOError):
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_file_approved(file_path: str, state: dict) -> tuple[bool, str]:
|
|
57
|
+
"""Check if file is approved for modification"""
|
|
58
|
+
|
|
59
|
+
# Normalize path
|
|
60
|
+
file_path = file_path.replace('\\', '/')
|
|
61
|
+
|
|
62
|
+
# Always blocked files
|
|
63
|
+
for blocked in ALWAYS_BLOCKED:
|
|
64
|
+
if blocked in file_path:
|
|
65
|
+
return False, f"BLOCKED: {file_path} is in the always-blocked list (sensitive file)"
|
|
66
|
+
|
|
67
|
+
# Always allowed files
|
|
68
|
+
for allowed in ALWAYS_ALLOWED:
|
|
69
|
+
if allowed in file_path:
|
|
70
|
+
return True, "Allowed (system file)"
|
|
71
|
+
|
|
72
|
+
# No active task - block all modifications
|
|
73
|
+
if not state or not state.get('currentTask'):
|
|
74
|
+
return False, f"""BLOCKED: No active task in workflow-state.json
|
|
75
|
+
|
|
76
|
+
To modify files, you must first:
|
|
77
|
+
1. Start a task via orchestrator agent
|
|
78
|
+
2. Run analyzer agent to approve files for modification
|
|
79
|
+
3. The file '{file_path}' must be in the approvedFiles list
|
|
80
|
+
|
|
81
|
+
Current state: No task active. Start with orchestrator first."""
|
|
82
|
+
|
|
83
|
+
task = state['currentTask']
|
|
84
|
+
approved_files = task.get('approvedFiles', [])
|
|
85
|
+
|
|
86
|
+
# Check if analyzer has run
|
|
87
|
+
agents = task.get('agents', {})
|
|
88
|
+
analyzer = agents.get('analyzer', {})
|
|
89
|
+
|
|
90
|
+
if not analyzer.get('executed'):
|
|
91
|
+
return False, f"""BLOCKED: Analyzer agent has not executed yet
|
|
92
|
+
|
|
93
|
+
To modify '{file_path}', you must:
|
|
94
|
+
1. Run the analyzer agent first
|
|
95
|
+
2. Analyzer will determine which files can be modified
|
|
96
|
+
3. Add approved files to workflow-state.json
|
|
97
|
+
|
|
98
|
+
Current task: {task.get('description', 'Unknown')}
|
|
99
|
+
Analyzer status: Not executed"""
|
|
100
|
+
|
|
101
|
+
# Check if file is in approved list
|
|
102
|
+
for approved in approved_files:
|
|
103
|
+
# Support glob patterns
|
|
104
|
+
if approved.endswith('*'):
|
|
105
|
+
if file_path.startswith(approved[:-1]):
|
|
106
|
+
return True, f"Approved (matches pattern: {approved})"
|
|
107
|
+
elif approved == file_path or file_path.endswith(approved):
|
|
108
|
+
return True, f"Approved by analyzer"
|
|
109
|
+
|
|
110
|
+
return False, f"""BLOCKED: File not in approved list
|
|
111
|
+
|
|
112
|
+
File: {file_path}
|
|
113
|
+
Task: {task.get('description', 'Unknown')}
|
|
114
|
+
|
|
115
|
+
Approved files for this task:
|
|
116
|
+
{chr(10).join(f' - {f}' for f in approved_files) if approved_files else ' (none)'}
|
|
117
|
+
|
|
118
|
+
To modify this file, run analyzer again and add it to approvedFiles."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main():
|
|
122
|
+
# Read hook input from stdin
|
|
123
|
+
try:
|
|
124
|
+
hook_input = json.load(sys.stdin)
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
# Can't parse input, allow by default
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
tool_name = hook_input.get('tool_name', '')
|
|
130
|
+
tool_input = hook_input.get('tool_input', {})
|
|
131
|
+
|
|
132
|
+
# Only check file modification tools
|
|
133
|
+
if tool_name not in FILE_MODIFY_TOOLS:
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
# Get file path from tool input
|
|
137
|
+
file_path = tool_input.get('file_path', tool_input.get('notebook_path', ''))
|
|
138
|
+
|
|
139
|
+
if not file_path:
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
|
|
142
|
+
# Load workflow state
|
|
143
|
+
state = load_workflow_state()
|
|
144
|
+
|
|
145
|
+
# Check if file is approved
|
|
146
|
+
approved, message = is_file_approved(file_path, state)
|
|
147
|
+
|
|
148
|
+
if approved:
|
|
149
|
+
# Log the approval (optional verbose output)
|
|
150
|
+
# print(f"✓ {message}", file=sys.stderr)
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
else:
|
|
153
|
+
# Block with error message
|
|
154
|
+
print(message, file=sys.stderr)
|
|
155
|
+
sys.exit(2)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == '__main__':
|
|
159
|
+
main()
|