@skilly-hand/skilly-hand 0.7.0 → 0.8.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/CHANGELOG.md +17 -0
- package/catalog/README.md +3 -0
- package/catalog/catalog-index.json +3 -0
- package/catalog/skills/frontend-design/SKILL.md +237 -0
- package/catalog/skills/frontend-design/agents/component-designer.md +95 -0
- package/catalog/skills/frontend-design/agents/stack-detector.md +154 -0
- package/catalog/skills/frontend-design/assets/stack-scan-checklist.md +58 -0
- package/catalog/skills/frontend-design/manifest.json +27 -0
- package/catalog/skills/life-guard/SKILL.md +180 -0
- package/catalog/skills/life-guard/assets/committee-member-template.md +44 -0
- package/catalog/skills/life-guard/assets/safety-guard-template.md +47 -0
- package/catalog/skills/life-guard/manifest.json +33 -0
- package/catalog/skills/test-driven-development/SKILL.md +159 -0
- package/catalog/skills/test-driven-development/assets/tdd-cycle.md +487 -0
- package/catalog/skills/test-driven-development/manifest.json +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# TDD Templates: RED → GREEN → REFACTOR Cycles
|
|
2
|
+
|
|
3
|
+
Real examples of the RED → GREEN → REFACTOR TDD cycle for implementing components, services, and pipes.
|
|
4
|
+
|
|
5
|
+
**Pattern**: Write failing test → Write minimum code to pass → Refactor while tests stay green
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Example 1: Simple Pipe Component
|
|
10
|
+
|
|
11
|
+
### Scenario: AsyncCachePipe
|
|
12
|
+
Cache hot observables to prevent resubscription overhead.
|
|
13
|
+
|
|
14
|
+
#### Step 1: RED — Write Failing Test
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// async-cache.pipe.test.ts
|
|
18
|
+
import { TestBed } from '@angular/core/testing';
|
|
19
|
+
import { Subject, take } from 'rxjs';
|
|
20
|
+
import { AsyncCachePipe } from './async-cache.pipe';
|
|
21
|
+
|
|
22
|
+
describe('AsyncCachePipe', () => {
|
|
23
|
+
let pipe: AsyncCachePipe;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
TestBed.configureTestingModule({});
|
|
27
|
+
pipe = TestBed.runInInjectionContext(() => new AsyncCachePipe());
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should cache async values and replay on late subscriptions', (done) => {
|
|
31
|
+
// GIVEN: An observable that emits after 100ms
|
|
32
|
+
let emitCount = 0;
|
|
33
|
+
const source$ = new Subject<number>();
|
|
34
|
+
|
|
35
|
+
// WHEN: First subscription
|
|
36
|
+
const result$ = pipe.transform(source$);
|
|
37
|
+
const values: number[] = [];
|
|
38
|
+
|
|
39
|
+
const sub1 = result$.subscribe((val) => {
|
|
40
|
+
emitCount++;
|
|
41
|
+
values.push(val);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
source$.next(42);
|
|
46
|
+
}, 100);
|
|
47
|
+
|
|
48
|
+
// WHEN: Second subscription arrives after value is cached
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
const sub2 = result$.pipe(take(1)).subscribe(() => {
|
|
51
|
+
// THEN: Second subscriber gets cached value immediately
|
|
52
|
+
expect(emitCount).toBe(1);
|
|
53
|
+
expect(values[0]).toBe(42);
|
|
54
|
+
|
|
55
|
+
sub1.unsubscribe();
|
|
56
|
+
sub2.unsubscribe();
|
|
57
|
+
source$.complete();
|
|
58
|
+
done();
|
|
59
|
+
});
|
|
60
|
+
}, 200);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ❌ TEST FAILS HERE - transform() not implemented yet
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Run**: `npm test -- async-cache.pipe.test.ts`
|
|
68
|
+
```
|
|
69
|
+
FAIL async-cache.pipe.test.ts
|
|
70
|
+
should cache async values and replay on late subscriptions
|
|
71
|
+
TypeError: pipe.transform is not a function
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
✅ Test fails as expected (proves test is meaningful)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
#### Step 2: GREEN — Write Minimum Code
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// async-cache.pipe.ts
|
|
82
|
+
import { Pipe, PipeTransform } from '@angular/core';
|
|
83
|
+
import { Observable, ReplaySubject } from 'rxjs';
|
|
84
|
+
import { tap } from 'rxjs/operators';
|
|
85
|
+
|
|
86
|
+
@Pipe({
|
|
87
|
+
name: 'asyncCache',
|
|
88
|
+
standalone: true,
|
|
89
|
+
})
|
|
90
|
+
export class AsyncCachePipe implements PipeTransform {
|
|
91
|
+
// ✅ MINIMUM: Just make the test pass
|
|
92
|
+
transform<T>(source: Observable<T>): Observable<T> {
|
|
93
|
+
const cached$ = new ReplaySubject<T>(1);
|
|
94
|
+
source.pipe(tap((val) => cached$.next(val))).subscribe();
|
|
95
|
+
return cached$.asObservable();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Run**: `npm test -- async-cache.pipe.test.ts`
|
|
101
|
+
```
|
|
102
|
+
PASS async-cache.pipe.test.ts
|
|
103
|
+
should cache async values and replay on late subscriptions ✓
|
|
104
|
+
|
|
105
|
+
Tests: 1 passed, 1 total
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
✅ Test passes (behavior is correct, but code needs cleanup)
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
#### Step 3: REFACTOR — Improve Without Changing Behavior
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// async-cache.pipe.ts (refactored)
|
|
116
|
+
import { Pipe, PipeTransform } from '@angular/core';
|
|
117
|
+
import { Observable, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
|
118
|
+
import { tap } from 'rxjs/operators';
|
|
119
|
+
|
|
120
|
+
@Pipe({
|
|
121
|
+
name: 'asyncCache',
|
|
122
|
+
standalone: true,
|
|
123
|
+
})
|
|
124
|
+
export class AsyncCachePipe implements PipeTransform {
|
|
125
|
+
private destroy$ = new Subject<void>();
|
|
126
|
+
|
|
127
|
+
// ✅ REFACTORED: Cleaner, adds cleanup
|
|
128
|
+
transform<T>(source: Observable<T>): Observable<T> {
|
|
129
|
+
const cacheSize = 1;
|
|
130
|
+
const cached$ = new ReplaySubject<T>(cacheSize);
|
|
131
|
+
|
|
132
|
+
source
|
|
133
|
+
.pipe(
|
|
134
|
+
tap((val) => cached$.next(val)),
|
|
135
|
+
takeUntil(this.destroy$)
|
|
136
|
+
)
|
|
137
|
+
.subscribe();
|
|
138
|
+
|
|
139
|
+
return cached$.asObservable();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ngOnDestroy(): void {
|
|
143
|
+
this.destroy$.next();
|
|
144
|
+
this.destroy$.complete();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Run**: `npm test -- async-cache.pipe.test.ts`
|
|
150
|
+
```
|
|
151
|
+
PASS async-cache.pipe.test.ts
|
|
152
|
+
should cache async values and replay on late subscriptions ✓
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
✅ Test still passes
|
|
156
|
+
|
|
157
|
+
**What improved**:
|
|
158
|
+
- Constants extracted (`cacheSize`)
|
|
159
|
+
- Cleanup logic added (`takeUntil`, `ngOnDestroy`)
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Example 2: Component with Input/Output
|
|
164
|
+
|
|
165
|
+
### Scenario: AlertComponent
|
|
166
|
+
Display a dismissible alert with a message.
|
|
167
|
+
|
|
168
|
+
#### Step 1: RED — Write Failing Test
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// alert.component.test.ts
|
|
172
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
173
|
+
import { AlertComponent } from './alert.component';
|
|
174
|
+
|
|
175
|
+
describe('AlertComponent', () => {
|
|
176
|
+
let component: AlertComponent;
|
|
177
|
+
let fixture: ComponentFixture<AlertComponent>;
|
|
178
|
+
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
await TestBed.configureTestingModule({
|
|
181
|
+
imports: [AlertComponent],
|
|
182
|
+
}).compileComponents();
|
|
183
|
+
|
|
184
|
+
fixture = TestBed.createComponent(AlertComponent);
|
|
185
|
+
component = fixture.componentInstance;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should display the alert message', () => {
|
|
189
|
+
// GIVEN: Component with message input
|
|
190
|
+
component.message = 'Error: Invalid input';
|
|
191
|
+
fixture.detectChanges();
|
|
192
|
+
|
|
193
|
+
// WHEN: Component renders
|
|
194
|
+
const messageEl = fixture.debugElement.nativeElement.querySelector('[data-testid="alert-message"]');
|
|
195
|
+
|
|
196
|
+
// THEN: Message is visible
|
|
197
|
+
expect(messageEl?.textContent).toBe('Error: Invalid input');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ❌ FAILS - Component doesn't exist yet
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Run**: `npm test -- alert.component.test.ts`
|
|
205
|
+
```
|
|
206
|
+
FAIL alert.component.test.ts
|
|
207
|
+
Cannot find component 'AlertComponent'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
#### Step 2: GREEN — Write Minimum Implementation
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// alert.component.ts
|
|
216
|
+
import { Component, input } from '@angular/core';
|
|
217
|
+
|
|
218
|
+
@Component({
|
|
219
|
+
selector: 'app-alert',
|
|
220
|
+
standalone: true,
|
|
221
|
+
template: `<div data-testid="alert-message">{{ message() }}</div>`,
|
|
222
|
+
})
|
|
223
|
+
export class AlertComponent {
|
|
224
|
+
// ✅ MINIMUM: Just the message input
|
|
225
|
+
message = input<string>('');
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Run**: `npm test -- alert.component.test.ts`
|
|
230
|
+
```
|
|
231
|
+
PASS alert.component.test.ts
|
|
232
|
+
should display the alert message ✓
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
#### Step 3: REFACTOR — Add Accessibility and Dismiss
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// alert.component.ts (refactored)
|
|
241
|
+
import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
|
|
242
|
+
|
|
243
|
+
@Component({
|
|
244
|
+
selector: 'app-alert',
|
|
245
|
+
standalone: true,
|
|
246
|
+
template: `
|
|
247
|
+
<div
|
|
248
|
+
class="alert"
|
|
249
|
+
role="alert"
|
|
250
|
+
aria-live="polite"
|
|
251
|
+
[attr.data-testid]="'alert-message'"
|
|
252
|
+
[class]="'alert--' + severity()">
|
|
253
|
+
<p class="alert__message">{{ message() }}</p>
|
|
254
|
+
<button class="alert__close" aria-label="Dismiss alert" (click)="onDismiss()">✕</button>
|
|
255
|
+
</div>
|
|
256
|
+
`,
|
|
257
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
258
|
+
})
|
|
259
|
+
export class AlertComponent {
|
|
260
|
+
message = input<string>('');
|
|
261
|
+
severity = input<'success' | 'error' | 'warning' | 'info'>('info');
|
|
262
|
+
dismissed = output<void>();
|
|
263
|
+
|
|
264
|
+
onDismiss(): void {
|
|
265
|
+
this.dismissed.emit();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Run**: `npm test -- alert.component.test.ts`
|
|
271
|
+
```
|
|
272
|
+
PASS alert.component.test.ts
|
|
273
|
+
should display the alert message ✓
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
✅ Test still passes
|
|
277
|
+
|
|
278
|
+
**What improved**:
|
|
279
|
+
- Accessibility attributes (`role="alert"`, `aria-live`, `aria-label`)
|
|
280
|
+
- Dismiss output and handler
|
|
281
|
+
- Severity levels
|
|
282
|
+
- `ChangeDetectionStrategy.OnPush`
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Example 3: Service with State Management
|
|
287
|
+
|
|
288
|
+
### Scenario: TodoService
|
|
289
|
+
Manage a todo list with add and complete operations.
|
|
290
|
+
|
|
291
|
+
#### Step 1: RED — Multiple Scenarios
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// todo.service.test.ts
|
|
295
|
+
import { TestBed } from '@angular/core/testing';
|
|
296
|
+
import { TodoService } from './todo.service';
|
|
297
|
+
|
|
298
|
+
describe('TodoService', () => {
|
|
299
|
+
let service: TodoService;
|
|
300
|
+
|
|
301
|
+
beforeEach(() => {
|
|
302
|
+
TestBed.configureTestingModule({ providers: [TodoService] });
|
|
303
|
+
service = TestBed.inject(TodoService);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should add a todo and update the list', () => {
|
|
307
|
+
// GIVEN: Empty list
|
|
308
|
+
// WHEN: Add todo
|
|
309
|
+
service.addTodo('Buy milk');
|
|
310
|
+
|
|
311
|
+
// THEN: Todo appears
|
|
312
|
+
expect(service.todos()).toContainEqual(
|
|
313
|
+
jasmine.objectContaining({ text: 'Buy milk', done: false })
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should mark a todo as complete', () => {
|
|
318
|
+
// GIVEN: A todo in the list
|
|
319
|
+
service.addTodo('Buy milk');
|
|
320
|
+
const firstTodo = service.todos()[0];
|
|
321
|
+
|
|
322
|
+
// WHEN: Mark as complete
|
|
323
|
+
service.completeTodo(firstTodo.id);
|
|
324
|
+
|
|
325
|
+
// THEN: Todo is done
|
|
326
|
+
expect(service.todos()[0].done).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ❌ FAILS - Service methods don't exist
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Run**: `npm test -- todo.service.test.ts`
|
|
334
|
+
```
|
|
335
|
+
FAIL todo.service.test.ts
|
|
336
|
+
service.addTodo is not a function
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
#### Step 2: GREEN — Write Minimum Implementation
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// todo.service.ts
|
|
345
|
+
import { Injectable, signal } from '@angular/core';
|
|
346
|
+
|
|
347
|
+
interface Todo { id: number; text: string; done: boolean; }
|
|
348
|
+
|
|
349
|
+
@Injectable({ providedIn: 'root' })
|
|
350
|
+
export class TodoService {
|
|
351
|
+
todos = signal<Todo[]>([]);
|
|
352
|
+
private nextId = 1;
|
|
353
|
+
|
|
354
|
+
addTodo(text: string): void {
|
|
355
|
+
this.todos.update((current) => [...current, { id: this.nextId++, text, done: false }]);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
completeTodo(id: number): void {
|
|
359
|
+
this.todos.update((current) =>
|
|
360
|
+
current.map((todo) => todo.id === id ? { ...todo, done: true } : todo)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Run**: `npm test -- todo.service.test.ts`
|
|
367
|
+
```
|
|
368
|
+
PASS todo.service.test.ts
|
|
369
|
+
should add a todo and update the list ✓
|
|
370
|
+
should mark a todo as complete ✓
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
#### Step 3: REFACTOR — Add Validation and Persistence
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// todo.service.ts (refactored)
|
|
379
|
+
import { Injectable, signal } from '@angular/core';
|
|
380
|
+
|
|
381
|
+
interface Todo {
|
|
382
|
+
id: number;
|
|
383
|
+
text: string;
|
|
384
|
+
done: boolean;
|
|
385
|
+
createdAt: Date;
|
|
386
|
+
completedAt?: Date;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@Injectable({ providedIn: 'root' })
|
|
390
|
+
export class TodoService {
|
|
391
|
+
todos = signal<Todo[]>([]);
|
|
392
|
+
private nextId = 1;
|
|
393
|
+
private readonly MAX_TODOS = 100;
|
|
394
|
+
|
|
395
|
+
constructor() {
|
|
396
|
+
this.loadFromStorage();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
addTodo(text: string): void {
|
|
400
|
+
if (!text?.trim()) throw new Error('Todo text cannot be empty');
|
|
401
|
+
if (this.todos().length >= this.MAX_TODOS) throw new Error(`Cannot exceed ${this.MAX_TODOS} todos`);
|
|
402
|
+
|
|
403
|
+
this.todos.update((current) => [
|
|
404
|
+
...current,
|
|
405
|
+
{ id: this.nextId++, text: text.trim(), done: false, createdAt: new Date() },
|
|
406
|
+
]);
|
|
407
|
+
this.saveToStorage();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
completeTodo(id: number): void {
|
|
411
|
+
if (!this.todos().find((t) => t.id === id)) throw new Error(`Todo with id ${id} not found`);
|
|
412
|
+
|
|
413
|
+
this.todos.update((current) =>
|
|
414
|
+
current.map((t) => t.id === id ? { ...t, done: true, completedAt: new Date() } : t)
|
|
415
|
+
);
|
|
416
|
+
this.saveToStorage();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
deleteTodo(id: number): void {
|
|
420
|
+
this.todos.update((current) => current.filter((t) => t.id !== id));
|
|
421
|
+
this.saveToStorage();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private loadFromStorage(): void {
|
|
425
|
+
try {
|
|
426
|
+
const stored = localStorage.getItem('todos');
|
|
427
|
+
if (stored) this.todos.set(JSON.parse(stored));
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.error('Failed to load todos from storage', e);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private saveToStorage(): void {
|
|
434
|
+
try {
|
|
435
|
+
localStorage.setItem('todos', JSON.stringify(this.todos()));
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.error('Failed to save todos to storage', e);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Run**: `npm test -- todo.service.test.ts`
|
|
444
|
+
```
|
|
445
|
+
PASS todo.service.test.ts
|
|
446
|
+
should add a todo and update the list ✓
|
|
447
|
+
should mark a todo as complete ✓
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
✅ Tests still pass
|
|
451
|
+
|
|
452
|
+
**What improved**:
|
|
453
|
+
- Input validation (empty check, max limit)
|
|
454
|
+
- Timestamps (`createdAt`, `completedAt`)
|
|
455
|
+
- Persistence via `localStorage`
|
|
456
|
+
- `deleteTodo` added for completeness
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Quick Checklist: RED → GREEN → REFACTOR
|
|
461
|
+
|
|
462
|
+
```markdown
|
|
463
|
+
### For Each Task Using TDD
|
|
464
|
+
|
|
465
|
+
#### RED Phase
|
|
466
|
+
- [ ] Write test that describes the feature
|
|
467
|
+
- [ ] Test has explicit GIVEN / WHEN / THEN
|
|
468
|
+
- [ ] Run tests — FAILS as expected
|
|
469
|
+
- [ ] Failure proves test is meaningful
|
|
470
|
+
|
|
471
|
+
#### GREEN Phase
|
|
472
|
+
- [ ] Write minimum code to pass
|
|
473
|
+
- [ ] No extra features beyond test requirement
|
|
474
|
+
- [ ] Run tests — PASSES
|
|
475
|
+
|
|
476
|
+
#### REFACTOR Phase
|
|
477
|
+
- [ ] Improve code structure / naming
|
|
478
|
+
- [ ] Extract constants, simplify logic
|
|
479
|
+
- [ ] Run tests — STILL PASSES
|
|
480
|
+
- [ ] No behavior changes, only improvements
|
|
481
|
+
|
|
482
|
+
#### Verify
|
|
483
|
+
- [ ] All tests pass: `npm test`
|
|
484
|
+
- [ ] Lint passes: `npm run lint`
|
|
485
|
+
- [ ] Type check: `npx tsc --noEmit`
|
|
486
|
+
- [ ] Build succeeds: `npm run build`
|
|
487
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "test-driven-development",
|
|
3
|
+
"title": "Test-Driven Development",
|
|
4
|
+
"description": "Guide implementation using the RED → GREEN → REFACTOR TDD cycle: write a failing test first, write the minimum code to pass, then refactor while tests stay green.",
|
|
5
|
+
"portable": true,
|
|
6
|
+
"tags": ["testing", "workflow", "quality", "core"],
|
|
7
|
+
"detectors": ["always"],
|
|
8
|
+
"detectionTriggers": ["manual"],
|
|
9
|
+
"installsFor": ["all"],
|
|
10
|
+
"agentSupport": ["codex", "claude", "cursor", "gemini", "copilot"],
|
|
11
|
+
"skillMetadata": {
|
|
12
|
+
"author": "skilly-hand",
|
|
13
|
+
"last-edit": "2026-04-04",
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"changelog": "Initial TDD skill ported from legacy scannlab-sdd tdd-templates; enables RED→GREEN→REFACTOR workflow across any stack; affects catalog skill coverage for test-first development",
|
|
17
|
+
"auto-invoke": "Implementing features, services, or components using test-driven development (TDD) or RED→GREEN→REFACTOR cycles",
|
|
18
|
+
"allowed-tools": ["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{ "path": "SKILL.md", "kind": "instruction" },
|
|
22
|
+
{ "path": "assets/tdd-cycle.md", "kind": "asset" }
|
|
23
|
+
],
|
|
24
|
+
"dependencies": []
|
|
25
|
+
}
|