oh-my-customcode 0.24.2 → 0.30.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 +3 -3
- package/dist/cli/index.js +518 -245
- package/dist/index.js +327 -37
- package/package.json +1 -1
- package/templates/.claude/agents/be-django-expert.md +45 -0
- package/templates/.claude/hooks/hooks.json +10 -0
- package/templates/.claude/hooks/scripts/context-budget-advisor.sh +86 -0
- package/templates/.claude/hooks/scripts/session-env-check.sh +58 -0
- package/templates/.claude/rules/SHOULD-ecomode.md +39 -0
- package/templates/.claude/rules/SHOULD-memory-integration.md +99 -9
- package/templates/.claude/skills/dev-lead-routing/SKILL.md +2 -1
- package/templates/.claude/skills/django-best-practices/SKILL.md +440 -0
- package/templates/CLAUDE.md.en +5 -5
- package/templates/CLAUDE.md.ko +5 -5
- package/templates/guides/django-best-practices/README.md +476 -0
- package/templates/manifest.json +4 -4
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# Django Best Practices Guide
|
|
2
|
+
|
|
3
|
+
> Reference: Django 6.0 Official Documentation + Community Best Practices
|
|
4
|
+
|
|
5
|
+
## Sources
|
|
6
|
+
|
|
7
|
+
- https://docs.djangoproject.com/en/6.0/
|
|
8
|
+
- https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
|
9
|
+
- https://github.com/HackSoftware/Django-Styleguide (HackSoft style guide)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Reference
|
|
14
|
+
|
|
15
|
+
### Project Setup
|
|
16
|
+
|
|
17
|
+
**Recommended project structure:**
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
project/
|
|
21
|
+
├── config/
|
|
22
|
+
│ ├── settings/
|
|
23
|
+
│ │ ├── base.py # Shared settings
|
|
24
|
+
│ │ ├── development.py # Dev overrides
|
|
25
|
+
│ │ └── production.py # Prod overrides
|
|
26
|
+
│ ├── urls.py
|
|
27
|
+
│ └── wsgi.py
|
|
28
|
+
├── apps/
|
|
29
|
+
│ ├── core/ # Shared utilities
|
|
30
|
+
│ ├── users/ # Custom User model
|
|
31
|
+
│ └── {feature}/ # Feature apps
|
|
32
|
+
├── templates/
|
|
33
|
+
├── static/
|
|
34
|
+
├── requirements/
|
|
35
|
+
│ ├── base.txt
|
|
36
|
+
│ ├── development.txt # + debug-toolbar, factory-boy
|
|
37
|
+
│ └── production.txt # + gunicorn, whitenoise
|
|
38
|
+
└── manage.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Settings split pattern:**
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# config/settings/base.py
|
|
45
|
+
SECRET_KEY = env('SECRET_KEY')
|
|
46
|
+
DEBUG = False
|
|
47
|
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
48
|
+
AUTH_USER_MODEL = 'users.User'
|
|
49
|
+
|
|
50
|
+
# config/settings/development.py
|
|
51
|
+
from .base import *
|
|
52
|
+
DEBUG = True
|
|
53
|
+
INSTALLED_APPS += ['debug_toolbar']
|
|
54
|
+
|
|
55
|
+
# config/settings/production.py
|
|
56
|
+
from .base import *
|
|
57
|
+
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
|
|
58
|
+
DATABASES = {'default': env.db()}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Always create a custom User model first:**
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# apps/users/models.py
|
|
65
|
+
from django.contrib.auth.models import AbstractUser
|
|
66
|
+
|
|
67
|
+
class User(AbstractUser):
|
|
68
|
+
pass # Extend later without pain
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### Models
|
|
74
|
+
|
|
75
|
+
**Model best practices:**
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from django.db import models
|
|
79
|
+
|
|
80
|
+
class Article(models.Model):
|
|
81
|
+
title = models.CharField(max_length=200, db_index=True)
|
|
82
|
+
body = models.TextField()
|
|
83
|
+
author = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
|
84
|
+
status = models.CharField(
|
|
85
|
+
max_length=20,
|
|
86
|
+
choices=[('draft', 'Draft'), ('published', 'Published')],
|
|
87
|
+
default='draft'
|
|
88
|
+
)
|
|
89
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
90
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
91
|
+
|
|
92
|
+
objects = models.Manager()
|
|
93
|
+
published = PublishedManager() # Custom manager
|
|
94
|
+
|
|
95
|
+
class Meta:
|
|
96
|
+
ordering = ['-created_at']
|
|
97
|
+
verbose_name = 'article'
|
|
98
|
+
verbose_name_plural = 'articles'
|
|
99
|
+
indexes = [
|
|
100
|
+
models.Index(fields=['status', 'created_at']),
|
|
101
|
+
]
|
|
102
|
+
constraints = [
|
|
103
|
+
models.CheckConstraint(
|
|
104
|
+
check=~models.Q(title=''),
|
|
105
|
+
name='article_title_not_empty'
|
|
106
|
+
)
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
def __str__(self):
|
|
110
|
+
return self.title
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Query optimization:**
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# N+1 prevention
|
|
117
|
+
articles = Article.objects.select_related('author').prefetch_related('tags')
|
|
118
|
+
|
|
119
|
+
# Partial field loading
|
|
120
|
+
titles = Article.objects.values_list('id', 'title') # No ORM object
|
|
121
|
+
|
|
122
|
+
# Bulk operations (never loop .save())
|
|
123
|
+
Article.objects.bulk_create(articles, batch_size=1000)
|
|
124
|
+
Article.objects.bulk_update(articles, ['status'], batch_size=1000)
|
|
125
|
+
|
|
126
|
+
# Complex queries with F() and Q()
|
|
127
|
+
from django.db.models import F, Q
|
|
128
|
+
Article.objects.filter(Q(status='published') | Q(author=request.user))
|
|
129
|
+
Article.objects.update(view_count=F('view_count') + 1)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### Views & URLs
|
|
135
|
+
|
|
136
|
+
**Class-Based Views for standard CRUD:**
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
140
|
+
from django.views.generic import ListView, DetailView, CreateView
|
|
141
|
+
|
|
142
|
+
class ArticleListView(ListView):
|
|
143
|
+
model = Article
|
|
144
|
+
template_name = 'articles/list.html'
|
|
145
|
+
context_object_name = 'articles'
|
|
146
|
+
paginate_by = 20
|
|
147
|
+
|
|
148
|
+
def get_queryset(self):
|
|
149
|
+
return Article.published.select_related('author')
|
|
150
|
+
|
|
151
|
+
class ArticleCreateView(LoginRequiredMixin, CreateView):
|
|
152
|
+
model = Article
|
|
153
|
+
form_class = ArticleForm
|
|
154
|
+
template_name = 'articles/form.html'
|
|
155
|
+
|
|
156
|
+
def form_valid(self, form):
|
|
157
|
+
form.instance.author = self.request.user
|
|
158
|
+
return super().form_valid(form)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**URL namespacing (required):**
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# apps/articles/urls.py
|
|
165
|
+
app_name = 'articles' # REQUIRED
|
|
166
|
+
|
|
167
|
+
urlpatterns = [
|
|
168
|
+
path('', ArticleListView.as_view(), name='list'),
|
|
169
|
+
path('<int:pk>/', ArticleDetailView.as_view(), name='detail'),
|
|
170
|
+
path('create/', ArticleCreateView.as_view(), name='create'),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
# Usage: reverse('articles:detail', args=[pk])
|
|
174
|
+
# Template: {% url 'articles:detail' article.pk %}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Security Checklist
|
|
180
|
+
|
|
181
|
+
Run before every production deployment: `python manage.py check --deploy`
|
|
182
|
+
|
|
183
|
+
**Required production settings:**
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# config/settings/production.py
|
|
187
|
+
|
|
188
|
+
# Core
|
|
189
|
+
DEBUG = False
|
|
190
|
+
SECRET_KEY = env('SECRET_KEY')
|
|
191
|
+
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
|
|
192
|
+
|
|
193
|
+
# HTTPS
|
|
194
|
+
SECURE_SSL_REDIRECT = True
|
|
195
|
+
SECURE_HSTS_SECONDS = 31536000
|
|
196
|
+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
197
|
+
SECURE_HSTS_PRELOAD = True
|
|
198
|
+
|
|
199
|
+
# Cookies
|
|
200
|
+
SESSION_COOKIE_SECURE = True
|
|
201
|
+
CSRF_COOKIE_SECURE = True
|
|
202
|
+
SESSION_COOKIE_HTTPONLY = True
|
|
203
|
+
CSRF_COOKIE_HTTPONLY = True
|
|
204
|
+
|
|
205
|
+
# Content security
|
|
206
|
+
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
207
|
+
X_FRAME_OPTIONS = 'DENY'
|
|
208
|
+
SECURE_BROWSER_XSS_FILTER = True
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Security built-ins (never disable):**
|
|
212
|
+
|
|
213
|
+
| Protection | Middleware / Setting | Default |
|
|
214
|
+
|------------|---------------------|---------|
|
|
215
|
+
| CSRF | `CsrfViewMiddleware` | On |
|
|
216
|
+
| XSS | Template auto-escaping | On |
|
|
217
|
+
| SQL injection | ORM parameterized queries | On |
|
|
218
|
+
| Clickjacking | `XFrameOptionsMiddleware` | On |
|
|
219
|
+
| Session security | `SessionMiddleware` | On |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### Performance
|
|
224
|
+
|
|
225
|
+
**N+1 query prevention:**
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
# Bad: N+1
|
|
229
|
+
for article in Article.objects.all():
|
|
230
|
+
print(article.author.username) # 1 query per article
|
|
231
|
+
|
|
232
|
+
# Good: 2 queries total
|
|
233
|
+
for article in Article.objects.select_related('author'):
|
|
234
|
+
print(article.author.username)
|
|
235
|
+
|
|
236
|
+
# Good: M2M prefetch
|
|
237
|
+
Article.objects.prefetch_related('tags', 'comments__author')
|
|
238
|
+
|
|
239
|
+
# Advanced: Custom prefetch
|
|
240
|
+
from django.db.models import Prefetch
|
|
241
|
+
Article.objects.prefetch_related(
|
|
242
|
+
Prefetch('comments', queryset=Comment.objects.filter(approved=True))
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Caching:**
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# Settings
|
|
250
|
+
CACHES = {
|
|
251
|
+
'default': {
|
|
252
|
+
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
|
253
|
+
'LOCATION': env('REDIS_URL'),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# View-level caching
|
|
258
|
+
from django.views.decorators.cache import cache_page
|
|
259
|
+
|
|
260
|
+
@cache_page(60 * 15) # 15 minutes
|
|
261
|
+
def my_view(request):
|
|
262
|
+
...
|
|
263
|
+
|
|
264
|
+
# Low-level API
|
|
265
|
+
from django.core.cache import cache
|
|
266
|
+
data = cache.get_or_set('my_key', expensive_function, timeout=300)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### Testing
|
|
272
|
+
|
|
273
|
+
**pytest-django setup:**
|
|
274
|
+
|
|
275
|
+
```ini
|
|
276
|
+
# pytest.ini
|
|
277
|
+
[pytest]
|
|
278
|
+
DJANGO_SETTINGS_MODULE = config.settings.test
|
|
279
|
+
python_files = tests/test_*.py
|
|
280
|
+
python_classes = Test*
|
|
281
|
+
python_functions = test_*
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Factory pattern:**
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
# apps/articles/tests/factories.py
|
|
288
|
+
import factory
|
|
289
|
+
from factory.django import DjangoModelFactory
|
|
290
|
+
|
|
291
|
+
class UserFactory(DjangoModelFactory):
|
|
292
|
+
class Meta:
|
|
293
|
+
model = 'users.User'
|
|
294
|
+
username = factory.Sequence(lambda n: f'user{n}')
|
|
295
|
+
email = factory.LazyAttribute(lambda o: f'{o.username}@example.com')
|
|
296
|
+
|
|
297
|
+
class ArticleFactory(DjangoModelFactory):
|
|
298
|
+
class Meta:
|
|
299
|
+
model = 'articles.Article'
|
|
300
|
+
title = factory.Faker('sentence')
|
|
301
|
+
author = factory.SubFactory(UserFactory)
|
|
302
|
+
status = 'published'
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Test structure:**
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
# apps/articles/tests/test_views.py
|
|
309
|
+
import pytest
|
|
310
|
+
from django.urls import reverse
|
|
311
|
+
|
|
312
|
+
@pytest.mark.django_db
|
|
313
|
+
class TestArticleListView:
|
|
314
|
+
def test_returns_published_articles(self, client):
|
|
315
|
+
ArticleFactory.create_batch(3, status='published')
|
|
316
|
+
ArticleFactory(status='draft')
|
|
317
|
+
|
|
318
|
+
url = reverse('articles:list')
|
|
319
|
+
response = client.get(url)
|
|
320
|
+
|
|
321
|
+
assert response.status_code == 200
|
|
322
|
+
assert len(response.context['articles']) == 3
|
|
323
|
+
|
|
324
|
+
def test_requires_login_for_create(self, client):
|
|
325
|
+
url = reverse('articles:create')
|
|
326
|
+
response = client.get(url)
|
|
327
|
+
assert response.status_code == 302 # Redirect to login
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### REST API (DRF)
|
|
333
|
+
|
|
334
|
+
**Serializers:**
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from rest_framework import serializers
|
|
338
|
+
|
|
339
|
+
class ArticleSerializer(serializers.ModelSerializer):
|
|
340
|
+
author_name = serializers.SerializerMethodField()
|
|
341
|
+
tags = serializers.StringRelatedField(many=True)
|
|
342
|
+
|
|
343
|
+
class Meta:
|
|
344
|
+
model = Article
|
|
345
|
+
fields = ['id', 'title', 'body', 'author_name', 'tags', 'created_at']
|
|
346
|
+
read_only_fields = ['id', 'created_at']
|
|
347
|
+
|
|
348
|
+
def get_author_name(self, obj):
|
|
349
|
+
return obj.author.get_full_name()
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**ViewSets + Routers:**
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
from rest_framework import viewsets, permissions
|
|
356
|
+
from rest_framework.decorators import action
|
|
357
|
+
from rest_framework.response import Response
|
|
358
|
+
|
|
359
|
+
class ArticleViewSet(viewsets.ModelViewSet):
|
|
360
|
+
queryset = Article.published.select_related('author')
|
|
361
|
+
serializer_class = ArticleSerializer
|
|
362
|
+
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
363
|
+
|
|
364
|
+
def perform_create(self, serializer):
|
|
365
|
+
serializer.save(author=self.request.user)
|
|
366
|
+
|
|
367
|
+
@action(detail=True, methods=['post'])
|
|
368
|
+
def publish(self, request, pk=None):
|
|
369
|
+
article = self.get_object()
|
|
370
|
+
article.status = 'published'
|
|
371
|
+
article.save()
|
|
372
|
+
return Response({'status': 'published'})
|
|
373
|
+
|
|
374
|
+
# urls.py
|
|
375
|
+
from rest_framework.routers import DefaultRouter
|
|
376
|
+
|
|
377
|
+
router = DefaultRouter()
|
|
378
|
+
router.register('articles', ArticleViewSet, basename='article')
|
|
379
|
+
|
|
380
|
+
urlpatterns = [path('api/v1/', include(router.urls))]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Authentication (JWT):**
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
# settings/base.py
|
|
387
|
+
REST_FRAMEWORK = {
|
|
388
|
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
389
|
+
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
|
390
|
+
],
|
|
391
|
+
'DEFAULT_PERMISSION_CLASSES': [
|
|
392
|
+
'rest_framework.permissions.IsAuthenticated',
|
|
393
|
+
],
|
|
394
|
+
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
|
395
|
+
'PAGE_SIZE': 20,
|
|
396
|
+
'DEFAULT_THROTTLE_CLASSES': [
|
|
397
|
+
'rest_framework.throttling.AnonRateThrottle',
|
|
398
|
+
'rest_framework.throttling.UserRateThrottle',
|
|
399
|
+
],
|
|
400
|
+
'DEFAULT_THROTTLE_RATES': {
|
|
401
|
+
'anon': '100/hour',
|
|
402
|
+
'user': '1000/hour',
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### Deployment
|
|
410
|
+
|
|
411
|
+
**Gunicorn configuration:**
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# gunicorn.conf.py
|
|
415
|
+
workers = multiprocessing.cpu_count() * 2 + 1
|
|
416
|
+
worker_class = 'sync' # or 'uvicorn.workers.UvicornWorker' for ASGI
|
|
417
|
+
bind = '0.0.0.0:8000'
|
|
418
|
+
timeout = 30
|
|
419
|
+
keepalive = 2
|
|
420
|
+
max_requests = 1000
|
|
421
|
+
max_requests_jitter = 100
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Static files with whitenoise:**
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
# settings/production.py
|
|
428
|
+
MIDDLEWARE = [
|
|
429
|
+
'django.middleware.security.SecurityMiddleware',
|
|
430
|
+
'whitenoise.middleware.WhiteNoiseMiddleware', # After SecurityMiddleware
|
|
431
|
+
...
|
|
432
|
+
]
|
|
433
|
+
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Deployment checklist:**
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
# Before every production deployment
|
|
440
|
+
python manage.py check --deploy
|
|
441
|
+
python manage.py migrate --run-syncdb
|
|
442
|
+
python manage.py collectstatic --noinput
|
|
443
|
+
|
|
444
|
+
# Database
|
|
445
|
+
# Use PostgreSQL (psycopg2-binary or psycopg[binary])
|
|
446
|
+
# Set up pgBouncer for connection pooling
|
|
447
|
+
|
|
448
|
+
# Logging
|
|
449
|
+
LOGGING = {
|
|
450
|
+
'version': 1,
|
|
451
|
+
'disable_existing_loggers': False,
|
|
452
|
+
'handlers': {
|
|
453
|
+
'console': {'class': 'logging.StreamHandler'},
|
|
454
|
+
'file': {'class': 'logging.FileHandler', 'filename': '/var/log/django.log'},
|
|
455
|
+
},
|
|
456
|
+
'root': {'handlers': ['console', 'file'], 'level': 'WARNING'},
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Package Recommendations
|
|
463
|
+
|
|
464
|
+
| Category | Package | Notes |
|
|
465
|
+
|----------|---------|-------|
|
|
466
|
+
| Settings | `django-environ` or `python-decouple` | Environment variable management |
|
|
467
|
+
| Auth | `djangorestframework-simplejwt` | JWT for APIs |
|
|
468
|
+
| API | `djangorestframework` | REST framework |
|
|
469
|
+
| Testing | `pytest-django`, `factory_boy` | Test infrastructure |
|
|
470
|
+
| Debug | `django-debug-toolbar` | Query inspection (dev only) |
|
|
471
|
+
| Static | `whitenoise` | Static file serving |
|
|
472
|
+
| Tasks | `celery` + `redis` | Background task queue |
|
|
473
|
+
| Caching | `django-redis` | Redis cache backend |
|
|
474
|
+
| Storage | `django-storages` + `boto3` | S3 media storage |
|
|
475
|
+
| Filtering | `django-filter` | DRF filter integration |
|
|
476
|
+
| Cors | `django-cors-headers` | CORS for SPA frontends |
|
package/templates/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.30.1",
|
|
3
3
|
"lastUpdated": "2026-03-09T00:00:00.000Z",
|
|
4
4
|
"components": [
|
|
5
5
|
{
|
|
@@ -12,19 +12,19 @@
|
|
|
12
12
|
"name": "agents",
|
|
13
13
|
"path": ".claude/agents",
|
|
14
14
|
"description": "AI agent definitions (flat .md files with prefixes)",
|
|
15
|
-
"files":
|
|
15
|
+
"files": 43
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"name": "skills",
|
|
19
19
|
"path": ".claude/skills",
|
|
20
20
|
"description": "Reusable skill modules (includes slash commands)",
|
|
21
|
-
"files":
|
|
21
|
+
"files": 67
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "guides",
|
|
25
25
|
"path": "guides",
|
|
26
26
|
"description": "Reference documentation",
|
|
27
|
-
"files":
|
|
27
|
+
"files": 23
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"name": "hooks",
|