@jjlabsio/claude-crew 0.1.11 → 0.1.13

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.
@@ -11,7 +11,7 @@
11
11
  "name": "claude-crew",
12
12
  "source": "./",
13
13
  "description": "오케스트레이터 + PM, 플래너, 개발, QA, 마케팅 에이전트 팀으로 단일 제품의 개발과 마케팅을 통합 관리",
14
- "version": "0.1.11",
14
+ "version": "0.1.13",
15
15
  "author": {
16
16
  "name": "Jaejin Song",
17
17
  "email": "wowlxx28@gmail.com"
@@ -28,5 +28,5 @@
28
28
  "category": "workflow"
29
29
  }
30
30
  ],
31
- "version": "0.1.11"
31
+ "version": "0.1.13"
32
32
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crew",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": {
6
6
  "name": "Jaejin Song",
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Delegation Enforcer — PreToolUse hook
4
+ *
5
+ * When an Agent/Task call includes subagent_type, auto-injects the model
6
+ * from the matching agent definition (agents/*.md frontmatter).
7
+ * If subagent_type is missing on an Agent/Task call, blocks with a warning.
8
+ */
9
+
10
+ import { readFileSync, readdirSync } from 'node:fs';
11
+ import { join, dirname } from 'node:path';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Read stdin
15
+ // ---------------------------------------------------------------------------
16
+ async function readStdin(timeoutMs = 3000) {
17
+ if (process.stdin.isTTY) return null;
18
+ return new Promise((resolve) => {
19
+ let data = '';
20
+ const timer = setTimeout(() => resolve(data || null), timeoutMs);
21
+ process.stdin.setEncoding('utf-8');
22
+ process.stdin.on('data', (chunk) => { data += chunk; });
23
+ process.stdin.on('end', () => { clearTimeout(timer); resolve(data || null); });
24
+ process.stdin.on('error', () => { clearTimeout(timer); resolve(null); });
25
+ });
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Load agent definitions from agents/*.md frontmatter
30
+ // ---------------------------------------------------------------------------
31
+ function loadAgentDefinitions(pluginRoot) {
32
+ const agentsDir = join(pluginRoot, 'agents');
33
+ const defs = {};
34
+
35
+ try {
36
+ const files = readdirSync(agentsDir).filter(f => f.endsWith('.md'));
37
+ for (const file of files) {
38
+ const content = readFileSync(join(agentsDir, file), 'utf-8');
39
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
40
+ if (!fmMatch) continue;
41
+
42
+ const fm = fmMatch[1];
43
+ const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim();
44
+ const model = fm.match(/^model:\s*(.+)$/m)?.[1]?.trim();
45
+
46
+ if (name && model) {
47
+ defs[name] = model;
48
+ }
49
+ }
50
+ } catch { /* ignore */ }
51
+
52
+ return defs;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Main
57
+ // ---------------------------------------------------------------------------
58
+ async function main() {
59
+ const raw = await readStdin();
60
+ if (!raw) {
61
+ console.log(JSON.stringify({ continue: true }));
62
+ return;
63
+ }
64
+
65
+ let event;
66
+ try { event = JSON.parse(raw); } catch {
67
+ console.log(JSON.stringify({ continue: true }));
68
+ return;
69
+ }
70
+
71
+ const toolName = (event.tool_name || '').toLowerCase();
72
+
73
+ // Only intercept Agent/Task calls
74
+ if (toolName !== 'agent' && toolName !== 'task') {
75
+ console.log(JSON.stringify({ continue: true }));
76
+ return;
77
+ }
78
+
79
+ const input = event.tool_input || {};
80
+
81
+ // If subagent_type is missing, pass through without modification
82
+ if (!input.subagent_type) {
83
+ console.log(JSON.stringify({ continue: true }));
84
+ return;
85
+ }
86
+
87
+ // Canonicalize subagent_type (strip plugin prefix if present)
88
+ const rawType = input.subagent_type.replace(/^claude-crew:/, '');
89
+
90
+ // Load agent definitions and auto-inject model if missing
91
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || dirname(import.meta.url.replace('file://', '')).replace('/hooks', '');
92
+ const agentDefs = loadAgentDefinitions(pluginRoot);
93
+
94
+ if (!input.model && agentDefs[rawType]) {
95
+ // Auto-inject model from agent definition
96
+ const injectedModel = agentDefs[rawType];
97
+ console.log(JSON.stringify({
98
+ continue: true,
99
+ modifiedInput: { ...input, model: injectedModel },
100
+ hookSpecificOutput: {
101
+ hookEventName: 'PreToolUse',
102
+ additionalContext: `model 자동 주입: ${rawType} → ${injectedModel}`,
103
+ },
104
+ }));
105
+ return;
106
+ }
107
+
108
+ console.log(JSON.stringify({ continue: true }));
109
+ }
110
+
111
+ main();
package/hooks/hooks.json CHANGED
@@ -12,6 +12,18 @@
12
12
  }
13
13
  ]
14
14
  }
15
+ ],
16
+ "PreToolUse": [
17
+ {
18
+ "matcher": "Agent|Task",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node \"$CLAUDE_PLUGIN_ROOT/hooks/enforce-delegation.mjs\"",
23
+ "timeout": 5
24
+ }
25
+ ]
26
+ }
15
27
  ]
16
28
  }
17
29
  }
package/hud/index.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { execSync } from 'node:child_process';
13
- import { readFileSync, existsSync } from 'node:fs';
13
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
14
14
  import { join, dirname, basename } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
16
 
@@ -57,6 +57,28 @@ function getVersion() {
57
57
  return '0.0.0';
58
58
  }
59
59
 
60
+ // ---------------------------------------------------------------------------
61
+ // Agent definitions (subagent_type → model)
62
+ // ---------------------------------------------------------------------------
63
+ function loadAgentModels() {
64
+ try {
65
+ const __dirname = dirname(fileURLToPath(import.meta.url));
66
+ const agentsDir = join(__dirname, '..', 'agents');
67
+ const models = {};
68
+ const files = readdirSync(agentsDir).filter(f => f.endsWith('.md'));
69
+ for (const file of files) {
70
+ const content = readFileSync(join(agentsDir, file), 'utf-8');
71
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
72
+ if (!fmMatch) continue;
73
+ const fm = fmMatch[1];
74
+ const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim();
75
+ const model = fm.match(/^model:\s*(.+)$/m)?.[1]?.trim();
76
+ if (name && model) models[name] = model;
77
+ }
78
+ return models;
79
+ } catch { return {}; }
80
+ }
81
+
60
82
  // ---------------------------------------------------------------------------
61
83
  // Git helpers
62
84
  // ---------------------------------------------------------------------------
@@ -144,6 +166,8 @@ function parseTranscript(transcriptPath) {
144
166
  const result = { agents: [], lastSkill: null, sessionStart: null };
145
167
  if (!transcriptPath || !existsSync(transcriptPath)) return result;
146
168
 
169
+ const agentModels = loadAgentModels();
170
+
147
171
  try {
148
172
  const content = readFileSync(transcriptPath, 'utf-8');
149
173
  const lines = content.split('\n').filter(Boolean);
@@ -178,7 +202,8 @@ function parseTranscript(transcriptPath) {
178
202
  if (id) {
179
203
  const input = block.input || {};
180
204
  const agentType = input.subagent_type || input.type || 'general';
181
- const model = input.model || null;
205
+ const rawType = agentType.replace(/^claude-crew:/, '');
206
+ const model = input.model || agentModels[rawType] || null;
182
207
  const description = input.description || input.prompt?.slice(0, 50) || '';
183
208
  const ts = entry.timestamp || lastTimestamp;
184
209
  agentMap.set(id, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlabsio/claude-crew",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": "Jaejin Song <wowlxx28@gmail.com>",
6
6
  "license": "MIT",
@@ -56,7 +56,7 @@ description: contract.md를 입력으로 받아 Dev + CodeReviewer + QA 파이
56
56
  | **CodeReviewer** | code-reviewer | git diff(직접 실행), 가드레일(인라인) | contract.md, plan.md, brief.md, spec.md, dev-log.md | 수용 기준 체리피킹 방지 (.crew/는 .gitignore 대상이므로 diff에 노출되지 않음) |
57
57
  | **QA** | qa | plan.md | contract.md, brief.md, spec.md | 검증 편향 방지 |
58
58
 
59
- **중요**: 모든 에이전트 호출 시 반드시 `subagent_type` 파라미터를 지정해야 한다. HUD에서 에이전트 타입을 식별하는 데 사용된다.
59
+ **중요**: 모든 에이전트 호출 시 반드시 `subagent_type` 파라미터를 지정해야 한다. `subagent_type`이 없으면 PreToolUse hook이 호출을 차단한다. `model` 파라미터는 생략 가능 — hook이 에이전트 정의에서 자동 주입한다.
60
60
 
61
61
  ---
62
62
 
@@ -107,7 +107,11 @@ IN_PROGRESS — Dev 에이전트가 구현 중이다.
107
107
 
108
108
  **모델**: opus
109
109
 
110
- 에이전트 호출 시 반드시 `subagent_type: "dev"`를 지정한다.
110
+ 호출:
111
+
112
+ ```
113
+ Agent(subagent_type="dev", description="Dev: {task-id} 구현", prompt="...")
114
+ ```
111
115
 
112
116
  **첫 번째 실행 시 에이전트 프롬프트**:
113
117
 
@@ -188,7 +192,11 @@ CodeReviewer와 QA를 **동시에** Agent tool 2개로 호출한다.
188
192
 
189
193
  **모델**: opus
190
194
 
191
- 에이전트 호출 시 반드시 `subagent_type: "code-reviewer"`를 지정한다.
195
+ 호출:
196
+
197
+ ```
198
+ Agent(subagent_type="code-reviewer", description="CodeReviewer: {task-id} 코드 리뷰", prompt="...")
199
+ ```
192
200
 
193
201
  오케스트레이터가 해야 할 사전 작업:
194
202
  1. contract.md에서 가드레일 섹션(Must/Must NOT)만 추출한다.
@@ -233,7 +241,11 @@ contract.md, plan.md, brief.md, spec.md, dev-log.md는 읽지 않는다.
233
241
 
234
242
  **모델**: sonnet
235
243
 
236
- 에이전트 호출 시 반드시 `subagent_type: "qa"`를 지정한다.
244
+ 호출:
245
+
246
+ ```
247
+ Agent(subagent_type="qa", description="QA: {task-id} 검증", prompt="...")
248
+ ```
237
249
 
238
250
  에이전트 프롬프트:
239
251
 
@@ -274,8 +286,8 @@ contract.md, brief.md, spec.md는 읽지 않는다.
274
286
  오케스트레이터는 한 번의 메시지에서 두 개의 Agent tool 호출을 동시에 수행한다:
275
287
 
276
288
  ```
277
- Agent(name="code-reviewer", subagent_type="code-reviewer", model="opus", prompt="...")
278
- Agent(name="qa", subagent_type="qa", model="sonnet", prompt="...")
289
+ Agent(subagent_type="code-reviewer", description="CodeReviewer: {task-id} 코드 리뷰", prompt="...")
290
+ Agent(subagent_type="qa", description="QA: {task-id} 검증", prompt="...")
279
291
  ```
280
292
 
281
293
  ---
@@ -63,7 +63,11 @@ brief.md가 없거나 비어 있습니다. 계획 파이프라인을 시작할
63
63
  **모델**: opus
64
64
  **건너뛰기 조건**: 엔지니어링 유형이면 이 단계를 건너뛴다.
65
65
 
66
- 에이전트 호출 시 반드시 `subagent_type: "pm"`을 지정한다.
66
+ 호출:
67
+
68
+ ```
69
+ Agent(subagent_type="pm", description="PM: {task-id} 요구사항 정의", prompt="...")
70
+ ```
67
71
 
68
72
  에이전트 프롬프트:
69
73
 
@@ -101,7 +105,11 @@ PM 에이전트가 요구사항 정의에 실패했습니다.
101
105
  **모델**: opus
102
106
  **실행 조건**: 항상 (양쪽 유형 모두)
103
107
 
104
- 에이전트 호출 시 반드시 `subagent_type: "techlead"`를 지정한다.
108
+ 호출:
109
+
110
+ ```
111
+ Agent(subagent_type="techlead", description="TechLead: {task-id} 사전 분석", prompt="...")
112
+ ```
105
113
 
106
114
  에이전트 프롬프트:
107
115
 
@@ -114,14 +122,14 @@ PM 에이전트가 요구사항 정의에 실패했습니다.
114
122
 
115
123
  ## 서브에이전트 호출
116
124
  - Explorer (Haiku): 코드베이스 탐색. 항상 호출. 병렬 2-3개로 호출하라.
117
- Agent(description="코드베이스 탐색", model="haiku", prompt="...")
125
+ Agent(subagent_type="explorer", description="코드베이스 탐색: {탐색 대상}", prompt="...")
118
126
  **필수 탐색 항목**: 테스트 인프라도 반드시 탐색한다. Explorer 중 1개는 다음을 확인:
119
127
  - 테스트 프레임워크 설정 파일 (jest.config.*, vitest.config.*, pytest.ini 등)
120
128
  - 대표적인 테스트 파일 2-3개의 패턴
121
129
  - 커버리지 설정 여부
122
130
  - 테스트 실행 스크립트 (package.json scripts 등)
123
131
  - Researcher (Sonnet): 외부 리서치. 필요시만 호출.
124
- Agent(description="외부 리서치", model="sonnet", prompt="...")
132
+ Agent(subagent_type="researcher", description="외부 리서치: {리서치 대상}", prompt="...")
125
133
 
126
134
  ## 출력
127
135
  .crew/plans/{task-id}/analysis.md 를 작성하라.
@@ -183,7 +191,11 @@ TechLead의 analysis.md에서 테스트 인프라 섹션을 확인한 후, 오
183
191
 
184
192
  **모델**: opus
185
193
 
186
- 에이전트 호출 시 반드시 `subagent_type: "planner"`를 지정한다.
194
+ 호출:
195
+
196
+ ```
197
+ Agent(subagent_type="planner", description="Planner: {task-id} 구현 계획", prompt="...")
198
+ ```
187
199
 
188
200
  **첫 번째 실행 시 에이전트 프롬프트**:
189
201
 
@@ -260,7 +272,11 @@ plan.md 최상단에 "이전 피드백 반영" 섹션을 추가한다.
260
272
 
261
273
  **모델**: sonnet (하드 임계값 판정에서 Opus 합리화 방지)
262
274
 
263
- 에이전트 호출 시 반드시 `subagent_type: "plan-evaluator"`를 지정한다.
275
+ 호출:
276
+
277
+ ```
278
+ Agent(subagent_type="plan-evaluator", description="PlanEvaluator: {task-id} 계획 검증", prompt="...")
279
+ ```
264
280
 
265
281
  에이전트 프롬프트:
266
282
 
@@ -295,7 +311,7 @@ plan.md 최상단에 "이전 피드백 반영" 섹션을 추가한다.
295
311
 
296
312
  ## E3 코드 참조 확인
297
313
  Explorer 서브에이전트를 호출하여 plan.md에서 참조하는 파일/모듈이 존재하는지 확인하라.
298
- Agent(description="코드 참조 확인", model="haiku", prompt="plan.md에서 참조하는 다음 파일/모듈이 존재하는지 확인하라: {파일 목록}")
314
+ Agent(subagent_type="explorer", description="코드 참조 확인: {파일 목록 요약}", prompt="plan.md에서 참조하는 다음 파일/모듈이 존재하는지 확인하라: {파일 목록}")
299
315
 
300
316
  ## 출력
301
317
  .crew/plans/{task-id}/review.md 를 작성하라.
@@ -458,7 +474,7 @@ Planner + PlanEvaluator 사이클은 최대 5회 (초기 1회 + retry 최대 4
458
474
  | Planner (retry) | planner | opus | spec.md (유저 가치 시) + analysis.md + review-{n}.md | brief.md |
459
475
  | PlanEvaluator | plan-evaluator | sonnet | spec.md/brief.md + analysis.md + plan.md | brief.md (유저 가치 시) |
460
476
 
461
- **중요**: 모든 에이전트 호출 시 반드시 `subagent_type` 파라미터를 지정해야 한다. HUD에서 에이전트 타입을 식별하는 데 사용된다.
477
+ **중요**: 모든 에이전트 호출 시 반드시 `subagent_type` 파라미터를 지정해야 한다. `subagent_type`이 없으면 PreToolUse hook이 호출을 차단한다. `model` 파라미터는 생략 가능 — hook이 에이전트 정의에서 자동 주입한다.
462
478
 
463
479
  ---
464
480