@su-record/vibe 2.9.38 → 2.10.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.
Files changed (134) hide show
  1. package/CLAUDE.md +19 -6
  2. package/README.md +31 -24
  3. package/agents/{teams/figma → figma}/figma-analyst.md +2 -2
  4. package/agents/research/{best-practices-agent.md → best-practices.md} +1 -1
  5. package/agents/research/{codebase-patterns-agent.md → codebase-patterns.md} +1 -1
  6. package/agents/research/{framework-docs-agent.md → framework-docs.md} +1 -1
  7. package/agents/research/{security-advisory-agent.md → security-advisory.md} +1 -1
  8. package/agents/teams/research-team.md +4 -4
  9. package/agents/teams/review-debate-team.md +2 -2
  10. package/agents/teams/security-team.md +4 -4
  11. package/dist/cli/commands/init.js +2 -2
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/postinstall/claude-agents.d.ts +3 -1
  14. package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
  15. package/dist/cli/postinstall/claude-agents.js +47 -9
  16. package/dist/cli/postinstall/claude-agents.js.map +1 -1
  17. package/dist/cli/postinstall/constants.d.ts +5 -0
  18. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  19. package/dist/cli/postinstall/constants.js +165 -23
  20. package/dist/cli/postinstall/constants.js.map +1 -1
  21. package/dist/cli/postinstall/cursor-skills.js +2 -2
  22. package/dist/cli/postinstall/main.d.ts.map +1 -1
  23. package/dist/cli/postinstall/main.js +19 -10
  24. package/dist/cli/postinstall/main.js.map +1 -1
  25. package/dist/infra/lib/OrchestrateWorkflow.js +1 -1
  26. package/dist/infra/lib/OrchestrateWorkflow.js.map +1 -1
  27. package/dist/infra/lib/telemetry/SkillTelemetry.test.js +4 -4
  28. package/dist/infra/lib/telemetry/SkillTelemetry.test.js.map +1 -1
  29. package/dist/infra/orchestrator/parallelResearch.js +4 -4
  30. package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
  31. package/hooks/scripts/__tests__/curation-index.test.js +157 -0
  32. package/hooks/scripts/__tests__/recipe-extractor.test.js +244 -0
  33. package/hooks/scripts/__tests__/step-counter.test.js +358 -0
  34. package/hooks/scripts/clone-extract.js +712 -0
  35. package/hooks/scripts/clone-refine.js +510 -0
  36. package/hooks/scripts/clone-to-scss.js +275 -0
  37. package/hooks/scripts/clone-validate.js +280 -0
  38. package/hooks/scripts/lib/curation-index.js +101 -0
  39. package/hooks/scripts/recipe-extractor.js +249 -0
  40. package/hooks/scripts/session-start.js +19 -0
  41. package/hooks/scripts/step-counter.js +230 -21
  42. package/package.json +2 -1
  43. package/skills/agents-md/SKILL.md +2 -0
  44. package/skills/arch-guard/SKILL.md +2 -0
  45. package/skills/brand-assets/SKILL.md +1 -0
  46. package/skills/capability-loop/SKILL.md +2 -0
  47. package/skills/characterization-test/SKILL.md +2 -0
  48. package/skills/chub-usage/SKILL.md +1 -0
  49. package/skills/claude-md-guide/SKILL.md +2 -0
  50. package/skills/clone/SKILL.md +361 -0
  51. package/skills/commerce-patterns/SKILL.md +1 -0
  52. package/skills/commit-push-pr/SKILL.md +1 -0
  53. package/skills/context7-usage/SKILL.md +1 -0
  54. package/skills/{vibe-contract → contract}/SKILL.md +7 -8
  55. package/skills/create-prd/SKILL.md +1 -0
  56. package/skills/design-audit/SKILL.md +1 -0
  57. package/skills/design-critique/SKILL.md +1 -0
  58. package/skills/design-distill/SKILL.md +1 -0
  59. package/skills/design-normalize/SKILL.md +1 -0
  60. package/skills/design-polish/SKILL.md +1 -0
  61. package/skills/design-teach/SKILL.md +2 -0
  62. package/skills/devlog/SKILL.md +1 -0
  63. package/skills/{vibe-docs → docs}/SKILL.md +5 -5
  64. package/skills/e2e-commerce/SKILL.md +1 -0
  65. package/skills/event-comms/SKILL.md +1 -0
  66. package/skills/event-ops/SKILL.md +1 -0
  67. package/skills/event-planning/SKILL.md +1 -0
  68. package/skills/exec-plan/SKILL.md +2 -0
  69. package/skills/{vibe-figma → figma}/SKILL.md +4 -3
  70. package/skills/{vibe-figma-convert → figma-convert}/SKILL.md +4 -3
  71. package/skills/{vibe-figma-extract → figma-extract}/SKILL.md +4 -3
  72. package/skills/git-worktree/SKILL.md +1 -0
  73. package/skills/handoff/SKILL.md +2 -0
  74. package/skills/{vibe-interview → interview}/SKILL.md +16 -16
  75. package/skills/parallel-research/SKILL.md +2 -0
  76. package/skills/{vibe-plan → plan}/SKILL.md +9 -9
  77. package/skills/prioritization-frameworks/SKILL.md +1 -0
  78. package/skills/priority-todos/SKILL.md +2 -0
  79. package/skills/{vibe-regress → regress}/SKILL.md +5 -6
  80. package/skills/rob-pike/SKILL.md +2 -0
  81. package/skills/seo-checklist/SKILL.md +1 -0
  82. package/skills/{vibe-spec → spec}/SKILL.md +14 -14
  83. package/skills/{vibe-spec-review → spec-review}/SKILL.md +8 -9
  84. package/skills/systematic-debugging/SKILL.md +2 -0
  85. package/skills/techdebt/SKILL.md +2 -0
  86. package/skills/{vibe-test → test}/SKILL.md +12 -12
  87. package/skills/tool-fallback/SKILL.md +1 -0
  88. package/skills/typescript-advanced-types/SKILL.md +1 -0
  89. package/skills/ui-ux-pro-max/SKILL.md +1 -0
  90. package/skills/user-personas/SKILL.md +1 -0
  91. package/skills/vercel-react-best-practices/SKILL.md +1 -0
  92. package/skills/vibe/SKILL.md +266 -0
  93. package/{commands/vibe.analyze.md → skills/vibe.analyze/SKILL.md} +2 -0
  94. package/skills/vibe.clone/SKILL.md +117 -0
  95. package/{commands/vibe.contract.md → skills/vibe.contract/SKILL.md} +3 -1
  96. package/{commands/vibe.docs.md → skills/vibe.docs/SKILL.md} +3 -1
  97. package/{commands/vibe.event.md → skills/vibe.event/SKILL.md} +2 -0
  98. package/{commands/vibe.figma.md → skills/vibe.figma/SKILL.md} +25 -23
  99. package/{commands/vibe.harness.md → skills/vibe.harness/SKILL.md} +2 -0
  100. package/{commands/vibe.reason.md → skills/vibe.reason/SKILL.md} +2 -0
  101. package/{commands/vibe.regress.md → skills/vibe.regress/SKILL.md} +5 -3
  102. package/{commands/vibe.review.md → skills/vibe.review/SKILL.md} +2 -0
  103. package/{commands/vibe.run.md → skills/vibe.run/SKILL.md} +3 -1
  104. package/{commands/vibe.scaffold.md → skills/vibe.scaffold/SKILL.md} +2 -0
  105. package/{commands/vibe.spec.md → skills/vibe.spec/SKILL.md} +36 -34
  106. package/{commands/vibe.test.md → skills/vibe.test/SKILL.md} +4 -2
  107. package/{commands/vibe.trace.md → skills/vibe.trace/SKILL.md} +7 -0
  108. package/{commands/vibe.utils.md → skills/vibe.utils/SKILL.md} +2 -0
  109. package/{commands/vibe.verify.md → skills/vibe.verify/SKILL.md} +10 -2
  110. package/skills/video-production/SKILL.md +1 -0
  111. /package/agents/{teams/figma → figma}/figma-architect.md +0 -0
  112. /package/agents/{teams/figma → figma}/figma-auditor.md +0 -0
  113. /package/agents/{teams/figma → figma}/figma-builder.md +0 -0
  114. /package/skills/{vibe-docs → docs}/templates/architecture.md +0 -0
  115. /package/skills/{vibe-docs → docs}/templates/behavioral-principles.md +0 -0
  116. /package/skills/{vibe-docs → docs}/templates/readme.md +0 -0
  117. /package/skills/{vibe-docs → docs}/templates/release-notes.md +0 -0
  118. /package/skills/{vibe-figma → figma}/rubrics/extraction-checklist.md +0 -0
  119. /package/skills/{vibe-figma → figma}/templates/component-index.md +0 -0
  120. /package/skills/{vibe-figma → figma}/templates/component-spec.md +0 -0
  121. /package/skills/{vibe-figma → figma}/templates/figma-handoff.md +0 -0
  122. /package/skills/{vibe-figma → figma}/templates/remapped-tree.md +0 -0
  123. /package/skills/{vibe-figma-convert → figma-convert}/rubrics/conversion-rules.md +0 -0
  124. /package/skills/{vibe-figma-convert → figma-convert}/templates/component.md +0 -0
  125. /package/skills/{vibe-figma-extract → figma-extract}/rubrics/image-rules.md +0 -0
  126. /package/skills/{vibe-interview → interview}/checklists/api.md +0 -0
  127. /package/skills/{vibe-interview → interview}/checklists/feature.md +0 -0
  128. /package/skills/{vibe-interview → interview}/checklists/library.md +0 -0
  129. /package/skills/{vibe-interview → interview}/checklists/mobile.md +0 -0
  130. /package/skills/{vibe-interview → interview}/checklists/webapp.md +0 -0
  131. /package/skills/{vibe-interview → interview}/checklists/website.md +0 -0
  132. /package/skills/{vibe-regress → regress}/templates/bug.md +0 -0
  133. /package/skills/{vibe-regress → regress}/templates/test-jest.md +0 -0
  134. /package/skills/{vibe-regress → regress}/templates/test-vitest.md +0 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * step-counter.js — Phase 1 + Phase 2 테스트
3
+ *
4
+ * 검증 범위:
5
+ * - 책임 1) current-run.json steps 증가 (회귀 테스트)
6
+ * - 책임 2) current-run.jsonl append (Phase 1)
7
+ * - 책임 3) error_category 분류기 + 3-fail detector → anti-pattern md (Phase 2)
8
+ * - 책임 간 독립성, hot-path 안정성
9
+ *
10
+ * 패턴: keyword-detector.test.js 와 동일한 execFileSync 방식.
11
+ */
12
+ import { describe, it, expect, beforeEach } from 'vitest';
13
+ import { spawnSync } from 'child_process';
14
+ import fs from 'fs';
15
+ import os from 'os';
16
+ import path from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const SCRIPT = path.resolve(__dirname, '..', 'step-counter.js');
21
+
22
+ /**
23
+ * 격리된 임시 프로젝트에서 step-counter 를 1회 실행.
24
+ * stdin 으로 PostToolUse payload 전달.
25
+ */
26
+ function runCounter({ payload, projectDir }) {
27
+ const result = spawnSync('node', [SCRIPT], {
28
+ input: payload ? JSON.stringify(payload) : '',
29
+ encoding: 'utf-8',
30
+ timeout: 5000,
31
+ env: {
32
+ ...process.env,
33
+ CLAUDE_PROJECT_DIR: projectDir,
34
+ // VIBE_HOOK_DEPTH 미설정 — 재귀 가드 영향 없음
35
+ },
36
+ });
37
+ return result;
38
+ }
39
+
40
+ function makeTmpProject() {
41
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-step-counter-'));
42
+ // .vibe/metrics/ 는 step-counter 가 직접 생성하도록 미리 만들지 않음
43
+ return dir;
44
+ }
45
+
46
+ function readJson(p) {
47
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
48
+ }
49
+
50
+ function readJsonl(p) {
51
+ return fs.readFileSync(p, 'utf-8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
52
+ }
53
+
54
+ describe('step-counter PostToolUse hook', () => {
55
+ let projectDir;
56
+ let runJson;
57
+ let runJsonl;
58
+
59
+ beforeEach(() => {
60
+ projectDir = makeTmpProject();
61
+ runJson = path.join(projectDir, '.vibe', 'metrics', 'current-run.json');
62
+ runJsonl = path.join(projectDir, '.vibe', 'metrics', 'current-run.jsonl');
63
+ });
64
+
65
+ // ───────── 책임 1 회귀 ─────────
66
+ describe('책임 1: current-run.json 카운터', () => {
67
+ it('첫 호출 시 steps=1, startedAt 채움', () => {
68
+ const r = runCounter({
69
+ payload: { tool_name: 'Bash', tool_input: { command: 'ls' }, tool_response: { is_error: false } },
70
+ projectDir,
71
+ });
72
+ expect(r.status).toBe(0);
73
+ const data = readJson(runJson);
74
+ expect(data.steps).toBe(1);
75
+ expect(data.startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
76
+ });
77
+
78
+ it('연속 호출 시 steps 누적', () => {
79
+ for (let i = 0; i < 3; i++) {
80
+ runCounter({
81
+ payload: { tool_name: 'Bash', tool_input: { command: 'echo hi' }, tool_response: {} },
82
+ projectDir,
83
+ });
84
+ }
85
+ const data = readJson(runJson);
86
+ expect(data.steps).toBe(3);
87
+ });
88
+
89
+ it('손상된 JSON 이 있어도 새로 시작', () => {
90
+ fs.mkdirSync(path.join(projectDir, '.vibe', 'metrics'), { recursive: true });
91
+ fs.writeFileSync(runJson, '{ broken json');
92
+ const r = runCounter({
93
+ payload: { tool_name: 'Bash', tool_input: {}, tool_response: {} },
94
+ projectDir,
95
+ });
96
+ expect(r.status).toBe(0);
97
+ const data = readJson(runJson);
98
+ expect(data.steps).toBe(1);
99
+ });
100
+ });
101
+
102
+ // ───────── 책임 2 신규 ─────────
103
+ describe('책임 2: current-run.jsonl 로깅', () => {
104
+ it('툴콜 1회 = jsonl 1라인', () => {
105
+ runCounter({
106
+ payload: { tool_name: 'Edit', tool_input: { file_path: 'src/foo.ts' }, tool_response: {} },
107
+ projectDir,
108
+ });
109
+ const lines = readJsonl(runJsonl);
110
+ expect(lines).toHaveLength(1);
111
+ expect(lines[0].tool).toBe('Edit');
112
+ expect(lines[0].ok).toBe(true);
113
+ expect(lines[0].target_file).toBe('src/foo.ts');
114
+ expect(lines[0].error_category).toBeNull();
115
+ expect(lines[0].ts).toMatch(/^\d{4}-\d{2}-\d{2}T/);
116
+ });
117
+
118
+ it('tool_response.is_error=true 면 ok=false', () => {
119
+ runCounter({
120
+ payload: { tool_name: 'Bash', tool_input: { command: 'false' }, tool_response: { is_error: true } },
121
+ projectDir,
122
+ });
123
+ const [line] = readJsonl(runJsonl);
124
+ expect(line.ok).toBe(false);
125
+ });
126
+
127
+ it('절대 경로 file_path 를 프로젝트 상대 경로로 정규화', () => {
128
+ const abs = path.join(projectDir, 'src', 'bar.ts');
129
+ runCounter({
130
+ payload: { tool_name: 'Write', tool_input: { file_path: abs }, tool_response: {} },
131
+ projectDir,
132
+ });
133
+ const [line] = readJsonl(runJsonl);
134
+ expect(line.target_file).toBe('src/bar.ts');
135
+ });
136
+
137
+ it('file_path 없는 툴콜은 target_file=null', () => {
138
+ runCounter({
139
+ payload: { tool_name: 'Bash', tool_input: { command: 'pwd' }, tool_response: {} },
140
+ projectDir,
141
+ });
142
+ const [line] = readJsonl(runJsonl);
143
+ expect(line.target_file).toBeNull();
144
+ });
145
+
146
+ it('tool_name 없으면 jsonl 라인 안 씀 (steps 는 증가)', () => {
147
+ runCounter({ payload: {}, projectDir });
148
+ expect(fs.existsSync(runJsonl)).toBe(false);
149
+ expect(readJson(runJson).steps).toBe(1);
150
+ });
151
+
152
+ it('연속 호출 시 jsonl 누적', () => {
153
+ runCounter({
154
+ payload: { tool_name: 'Read', tool_input: { file_path: 'a.ts' }, tool_response: {} },
155
+ projectDir,
156
+ });
157
+ runCounter({
158
+ payload: { tool_name: 'Edit', tool_input: { file_path: 'a.ts' }, tool_response: {} },
159
+ projectDir,
160
+ });
161
+ const lines = readJsonl(runJsonl);
162
+ expect(lines).toHaveLength(2);
163
+ expect(lines.map((l) => l.tool)).toEqual(['Read', 'Edit']);
164
+ });
165
+ });
166
+
167
+ // ───────── 독립성 ─────────
168
+ describe('두 책임의 독립성', () => {
169
+ it('payload 가 비어 stdin 무효여도 카운터는 증가', () => {
170
+ const r = runCounter({ payload: null, projectDir });
171
+ expect(r.status).toBe(0);
172
+ expect(readJson(runJson).steps).toBe(1);
173
+ });
174
+ });
175
+
176
+ // ───────── Phase 2: error_category 분류 ─────────
177
+ describe('책임 3a: error_category 분류기', () => {
178
+ it('TypeError of undefined → nullability', () => {
179
+ runCounter({
180
+ payload: {
181
+ tool_name: 'Bash',
182
+ tool_input: { command: 'node x.js' },
183
+ tool_response: { is_error: true, error: "TypeError: Cannot read properties of undefined (reading 'foo')" },
184
+ },
185
+ projectDir,
186
+ });
187
+ const [line] = readJsonl(runJsonl);
188
+ expect(line.error_category).toBe('nullability');
189
+ });
190
+
191
+ it('TS2345 → type-narrow', () => {
192
+ runCounter({
193
+ payload: {
194
+ tool_name: 'Bash',
195
+ tool_input: { command: 'tsc' },
196
+ tool_response: { is_error: true, error: "src/x.ts(3,4): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'." },
197
+ },
198
+ projectDir,
199
+ });
200
+ const [line] = readJsonl(runJsonl);
201
+ expect(line.error_category).toBe('type-narrow');
202
+ });
203
+
204
+ it('ECONNREFUSED → integration', () => {
205
+ runCounter({
206
+ payload: {
207
+ tool_name: 'Bash',
208
+ tool_input: { command: 'curl' },
209
+ tool_response: { is_error: true, error: 'connect ECONNREFUSED 127.0.0.1:5432' },
210
+ },
211
+ projectDir,
212
+ });
213
+ const [line] = readJsonl(runJsonl);
214
+ expect(line.error_category).toBe('integration');
215
+ });
216
+
217
+ it('알 수 없는 에러 → other', () => {
218
+ runCounter({
219
+ payload: {
220
+ tool_name: 'Bash',
221
+ tool_input: { command: 'x' },
222
+ tool_response: { is_error: true, error: 'some unrecognized failure mode 12345' },
223
+ },
224
+ projectDir,
225
+ });
226
+ const [line] = readJsonl(runJsonl);
227
+ expect(line.error_category).toBe('other');
228
+ });
229
+
230
+ it('성공 툴콜은 error_category=null', () => {
231
+ runCounter({
232
+ payload: { tool_name: 'Bash', tool_input: { command: 'ls' }, tool_response: { is_error: false } },
233
+ projectDir,
234
+ });
235
+ const [line] = readJsonl(runJsonl);
236
+ expect(line.ok).toBe(true);
237
+ expect(line.error_category).toBeNull();
238
+ });
239
+ });
240
+
241
+ // ───────── Phase 2: 3-fail detector ─────────
242
+ describe('책임 3b: 3-fail detector → anti-pattern md', () => {
243
+ function failBash(command, errorText, projectDir) {
244
+ runCounter({
245
+ payload: {
246
+ tool_name: 'Bash',
247
+ tool_input: { command },
248
+ tool_response: { is_error: true, error: errorText },
249
+ },
250
+ projectDir,
251
+ });
252
+ }
253
+
254
+ function listAntiPatterns(projectDir) {
255
+ const dir = path.join(projectDir, '.vibe', 'anti-patterns');
256
+ if (!fs.existsSync(dir)) return [];
257
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
258
+ }
259
+
260
+ it('같은 (file, category) 3회 누적 시 anti-pattern md 생성', () => {
261
+ const err = "TypeError: Cannot read properties of undefined (reading 'x')";
262
+ // file_path 없는 Bash 실패 3회 → target_file=null, category=nullability
263
+ for (let i = 0; i < 3; i++) failBash(`run-${i}`, err, projectDir);
264
+ const files = listAntiPatterns(projectDir);
265
+ expect(files).toHaveLength(1);
266
+ expect(files[0]).toMatch(/^nullability__global__\d{8}\.md$/);
267
+ });
268
+
269
+ it('2회만 누적되면 생성 안 함', () => {
270
+ const err = 'connect ECONNREFUSED foo';
271
+ for (let i = 0; i < 2; i++) failBash(`x-${i}`, err, projectDir);
272
+ expect(listAntiPatterns(projectDir)).toHaveLength(0);
273
+ });
274
+
275
+ it('3회지만 카테고리 다르면 생성 안 함', () => {
276
+ failBash('a', "Cannot read properties of undefined", projectDir); // nullability
277
+ failBash('b', "ECONNREFUSED", projectDir); // integration
278
+ failBash('c', "TS2345 not assignable", projectDir); // type-narrow
279
+ expect(listAntiPatterns(projectDir)).toHaveLength(0);
280
+ });
281
+
282
+ it('동일 (file, category) 4회 — md 1개만 (dedup)', () => {
283
+ const err = "TypeError: Cannot read properties of undefined";
284
+ for (let i = 0; i < 4; i++) failBash(`r-${i}`, err, projectDir);
285
+ expect(listAntiPatterns(projectDir)).toHaveLength(1);
286
+ });
287
+
288
+ it('frontmatter 가 schema 충족', () => {
289
+ const err = "TypeError: Cannot read properties of null";
290
+ for (let i = 0; i < 3; i++) failBash(`r-${i}`, err, projectDir);
291
+ const [filename] = listAntiPatterns(projectDir);
292
+ const content = fs.readFileSync(path.join(projectDir, '.vibe', 'anti-patterns', filename), 'utf-8');
293
+ expect(content).toMatch(/^---\n/);
294
+ expect(content).toMatch(/^slug: nullability__global__\d{8}$/m);
295
+ expect(content).toMatch(/^type: anti-pattern$/m);
296
+ expect(content).toMatch(/^root-cause-tag: nullability$/m);
297
+ expect(content).toMatch(/^trigger-signature: "/m);
298
+ expect(content).toMatch(/^fail-count: 3$/m);
299
+ expect(content).toMatch(/^suggested-stop: "/m);
300
+ expect(content).toMatch(/^created: \d{4}-\d{2}-\d{2}$/m);
301
+ });
302
+
303
+ it('파일 경로 있는 실패는 file slug 사용', () => {
304
+ const filePath = 'src/cli/foo.ts';
305
+ const err = "TypeError: Cannot read properties of undefined";
306
+ for (let i = 0; i < 3; i++) {
307
+ runCounter({
308
+ payload: {
309
+ tool_name: 'Edit',
310
+ tool_input: { file_path: filePath },
311
+ tool_response: { is_error: true, error: err },
312
+ },
313
+ projectDir,
314
+ });
315
+ }
316
+ const [filename] = listAntiPatterns(projectDir);
317
+ expect(filename).toMatch(/^nullability__src-cli-foo-ts__\d{8}\.md$/);
318
+ });
319
+
320
+ it('윈도우 외 실패는 카운트 안 됨', () => {
321
+ const err = "TypeError: Cannot read properties of undefined";
322
+ // 같은 카테고리 실패 2회
323
+ failBash('a', err, projectDir);
324
+ failBash('b', err, projectDir);
325
+ // 성공 툴콜로 윈도우 채움 (10줄 이상)
326
+ for (let i = 0; i < 10; i++) {
327
+ runCounter({
328
+ payload: { tool_name: 'Read', tool_input: { file_path: `f${i}.ts` }, tool_response: {} },
329
+ projectDir,
330
+ });
331
+ }
332
+ // 마지막 실패 1회 — 윈도우(10줄) 안에는 이 실패 + 직전 성공만 있으므로 트립 안 함
333
+ failBash('c', err, projectDir);
334
+ expect(listAntiPatterns(projectDir)).toHaveLength(0);
335
+ });
336
+ });
337
+
338
+ // ───────── 차단 금지 ─────────
339
+ describe('hot path 안정성', () => {
340
+ it('항상 exit 0', () => {
341
+ const r = runCounter({
342
+ payload: { tool_name: 'Bash', tool_input: { command: 'x' }, tool_response: {} },
343
+ projectDir,
344
+ });
345
+ expect(r.status).toBe(0);
346
+ });
347
+
348
+ it('잘못된 stdin JSON 도 차단 안 함', () => {
349
+ const r = spawnSync('node', [SCRIPT], {
350
+ input: 'not json at all',
351
+ encoding: 'utf-8',
352
+ timeout: 5000,
353
+ env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
354
+ });
355
+ expect(r.status).toBe(0);
356
+ });
357
+ });
358
+ });