@wooojin/forgen 0.2.0 → 0.3.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 (158) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.ja.md +79 -14
  3. package/README.ko.md +100 -14
  4. package/README.md +124 -17
  5. package/README.zh.md +79 -14
  6. package/agents/analyst.md +48 -4
  7. package/agents/architect.md +39 -4
  8. package/agents/code-reviewer.md +107 -77
  9. package/agents/critic.md +47 -4
  10. package/agents/debugger.md +46 -4
  11. package/agents/designer.md +40 -4
  12. package/agents/executor.md +112 -30
  13. package/agents/explore.md +45 -5
  14. package/agents/git-master.md +48 -4
  15. package/agents/planner.md +121 -18
  16. package/agents/test-engineer.md +58 -4
  17. package/agents/verifier.md +92 -77
  18. package/commands/architecture-decision.md +127 -258
  19. package/commands/calibrate.md +225 -0
  20. package/commands/code-review.md +163 -178
  21. package/commands/compound.md +127 -68
  22. package/commands/deep-interview.md +273 -0
  23. package/commands/docker.md +68 -178
  24. package/commands/forge-loop.md +215 -0
  25. package/commands/learn.md +231 -0
  26. package/commands/retro.md +215 -0
  27. package/commands/ship.md +277 -0
  28. package/dist/cli.js +26 -9
  29. package/dist/core/auto-compound-runner.js +14 -0
  30. package/dist/core/config-injector.d.ts +2 -1
  31. package/dist/core/config-injector.js +2 -1
  32. package/dist/core/dashboard.d.ts +108 -0
  33. package/dist/core/dashboard.js +495 -0
  34. package/dist/core/doctor.js +151 -21
  35. package/dist/core/drift-score.d.ts +49 -0
  36. package/dist/core/drift-score.js +87 -0
  37. package/dist/core/harness.d.ts +6 -1
  38. package/dist/core/harness.js +75 -19
  39. package/dist/core/mcp-config.d.ts +2 -0
  40. package/dist/core/mcp-config.js +6 -1
  41. package/dist/core/paths.d.ts +6 -1
  42. package/dist/core/paths.js +18 -2
  43. package/dist/core/spawn.d.ts +3 -2
  44. package/dist/core/spawn.js +27 -8
  45. package/dist/core/types.d.ts +34 -0
  46. package/dist/engine/compound-export.d.ts +41 -0
  47. package/dist/engine/compound-export.js +169 -0
  48. package/dist/engine/compound-lifecycle.d.ts +4 -3
  49. package/dist/engine/compound-lifecycle.js +91 -46
  50. package/dist/engine/compound-loop.js +18 -0
  51. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  52. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  53. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  54. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  55. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  56. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  57. package/dist/engine/meta-learning/runner.d.ts +14 -0
  58. package/dist/engine/meta-learning/runner.js +90 -0
  59. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  60. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  61. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  62. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  63. package/dist/engine/meta-learning/types.d.ts +114 -0
  64. package/dist/engine/meta-learning/types.js +43 -0
  65. package/dist/engine/solution-format.d.ts +2 -2
  66. package/dist/engine/solution-format.js +249 -34
  67. package/dist/engine/solution-index.d.ts +1 -1
  68. package/dist/engine/solution-matcher.d.ts +30 -1
  69. package/dist/engine/solution-matcher.js +235 -45
  70. package/dist/fgx.js +12 -8
  71. package/dist/hooks/context-guard.d.ts +15 -0
  72. package/dist/hooks/context-guard.js +218 -56
  73. package/dist/hooks/db-guard.js +2 -2
  74. package/dist/hooks/hook-config.d.ts +27 -1
  75. package/dist/hooks/hook-config.js +72 -12
  76. package/dist/hooks/hooks-generator.d.ts +3 -0
  77. package/dist/hooks/hooks-generator.js +23 -6
  78. package/dist/hooks/intent-classifier.d.ts +0 -2
  79. package/dist/hooks/intent-classifier.js +32 -18
  80. package/dist/hooks/keyword-detector.js +126 -204
  81. package/dist/hooks/notepad-injector.js +2 -2
  82. package/dist/hooks/permission-handler.js +2 -2
  83. package/dist/hooks/post-tool-failure.js +12 -6
  84. package/dist/hooks/post-tool-handlers.d.ts +1 -1
  85. package/dist/hooks/post-tool-handlers.js +14 -11
  86. package/dist/hooks/post-tool-use.d.ts +11 -0
  87. package/dist/hooks/post-tool-use.js +184 -71
  88. package/dist/hooks/pre-compact.d.ts +11 -1
  89. package/dist/hooks/pre-compact.js +112 -37
  90. package/dist/hooks/pre-tool-use.js +86 -56
  91. package/dist/hooks/rate-limiter.js +3 -3
  92. package/dist/hooks/secret-filter.js +2 -2
  93. package/dist/hooks/session-recovery.js +256 -236
  94. package/dist/hooks/shared/hook-response.d.ts +4 -4
  95. package/dist/hooks/shared/hook-response.js +13 -24
  96. package/dist/hooks/shared/hook-timing.d.ts +15 -0
  97. package/dist/hooks/shared/hook-timing.js +64 -0
  98. package/dist/hooks/skill-injector.d.ts +4 -3
  99. package/dist/hooks/skill-injector.js +47 -16
  100. package/dist/hooks/slop-detector.js +3 -3
  101. package/dist/hooks/solution-injector.js +224 -197
  102. package/dist/hooks/subagent-tracker.js +2 -2
  103. package/dist/host/codex-adapter.d.ts +10 -0
  104. package/dist/host/codex-adapter.js +154 -0
  105. package/dist/mcp/solution-reader.d.ts +5 -5
  106. package/dist/mcp/solution-reader.js +34 -24
  107. package/dist/renderer/rule-renderer.js +9 -11
  108. package/dist/services/session.d.ts +19 -0
  109. package/dist/services/session.js +62 -0
  110. package/hooks/hooks.json +2 -2
  111. package/package.json +2 -1
  112. package/skills/architecture-decision/SKILL.md +113 -257
  113. package/skills/calibrate/SKILL.md +207 -0
  114. package/skills/code-review/SKILL.md +151 -178
  115. package/skills/compound/SKILL.md +126 -68
  116. package/skills/deep-interview/SKILL.md +266 -0
  117. package/skills/docker/SKILL.md +57 -179
  118. package/skills/forge-loop/SKILL.md +198 -0
  119. package/skills/learn/SKILL.md +216 -0
  120. package/skills/retro/SKILL.md +199 -0
  121. package/skills/ship/SKILL.md +259 -0
  122. package/agents/code-simplifier.md +0 -197
  123. package/agents/performance-reviewer.md +0 -172
  124. package/agents/qa-tester.md +0 -158
  125. package/agents/refactoring-expert.md +0 -168
  126. package/agents/scientist.md +0 -144
  127. package/agents/security-reviewer.md +0 -137
  128. package/agents/writer.md +0 -184
  129. package/commands/api-design.md +0 -268
  130. package/commands/ci-cd.md +0 -270
  131. package/commands/database.md +0 -263
  132. package/commands/debug-detective.md +0 -99
  133. package/commands/documentation.md +0 -276
  134. package/commands/ecomode.md +0 -51
  135. package/commands/frontend.md +0 -271
  136. package/commands/git-master.md +0 -90
  137. package/commands/incident-response.md +0 -292
  138. package/commands/migrate.md +0 -101
  139. package/commands/performance.md +0 -288
  140. package/commands/refactor.md +0 -105
  141. package/commands/security-review.md +0 -288
  142. package/commands/tdd.md +0 -183
  143. package/commands/testing-strategy.md +0 -265
  144. package/skills/api-design/SKILL.md +0 -262
  145. package/skills/ci-cd/SKILL.md +0 -264
  146. package/skills/database/SKILL.md +0 -257
  147. package/skills/debug-detective/SKILL.md +0 -95
  148. package/skills/documentation/SKILL.md +0 -270
  149. package/skills/ecomode/SKILL.md +0 -46
  150. package/skills/frontend/SKILL.md +0 -265
  151. package/skills/git-master/SKILL.md +0 -86
  152. package/skills/incident-response/SKILL.md +0 -286
  153. package/skills/migrate/SKILL.md +0 -96
  154. package/skills/performance/SKILL.md +0 -282
  155. package/skills/refactor/SKILL.md +0 -100
  156. package/skills/security-review/SKILL.md +0 -282
  157. package/skills/tdd/SKILL.md +0 -178
  158. package/skills/testing-strategy/SKILL.md +0 -260
@@ -14,8 +14,8 @@
14
14
  * 모델에 컨텍스트를 주입하려면 반드시 additionalContext를 사용해야 함.
15
15
  */
16
16
  import * as fs from 'node:fs';
17
+ import * as os from 'node:os';
17
18
  import * as path from 'node:path';
18
- import { STATE_DIR } from '../../core/paths.js';
19
19
  /** 통과 응답 (컨텍스트 없음, 모든 이벤트 공통) */
20
20
  export function approve() {
21
21
  return JSON.stringify({ continue: true });
@@ -63,31 +63,20 @@ export function ask(reason) {
63
63
  export function failOpen() {
64
64
  return JSON.stringify({ continue: true });
65
65
  }
66
- /** 훅별 에러 카운트를 STATE_DIR/hook-errors.json에 누적 */
67
- export function incrementHookErrorCount(hookName) {
68
- try {
69
- const errorPath = path.join(STATE_DIR, 'hook-errors.json');
70
- let errors = {};
71
- try {
72
- if (fs.existsSync(errorPath)) {
73
- errors = JSON.parse(fs.readFileSync(errorPath, 'utf-8'));
74
- }
75
- }
76
- catch { /* start fresh */ }
77
- if (!errors[hookName])
78
- errors[hookName] = { count: 0, lastAt: '' };
79
- errors[hookName].count++;
80
- errors[hookName].lastAt = new Date().toISOString();
81
- fs.mkdirSync(STATE_DIR, { recursive: true });
82
- fs.writeFileSync(errorPath, JSON.stringify(errors, null, 2));
83
- }
84
- catch { /* meta-error in error tracking — ignore */ }
85
- }
86
66
  /**
87
- * fail-open + 에러 카운트 누적.
88
- * 훅의 main().catch() 블록에서 명시적으로 호출.
67
+ * fail-open with error tracking: 에러 안전하게 통과하되, 실패 정보를 기록.
68
+ * forgen doctor의 Hook Health 섹션에서 실패 이력을 표시할 수 있도록 JSONL 로그에 기록.
69
+ *
70
+ * @fail-open: hook failure must never block the user's workflow
89
71
  */
90
72
  export function failOpenWithTracking(hookName) {
91
- incrementHookErrorCount(hookName);
73
+ try {
74
+ const stateDir = path.join(os.homedir(), '.forgen', 'state');
75
+ fs.mkdirSync(stateDir, { recursive: true });
76
+ const logPath = path.join(stateDir, 'hook-errors.jsonl');
77
+ const entry = JSON.stringify({ hook: hookName, at: Date.now() });
78
+ fs.appendFileSync(logPath, entry + '\n');
79
+ }
80
+ catch { /* fail-open: tracking itself must not throw */ }
92
81
  return JSON.stringify({ continue: true });
93
82
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Forgen — Hook Timing Profiler
3
+ *
4
+ * Records hook execution durations and provides timing statistics
5
+ * for visibility into which hooks are slow.
6
+ */
7
+ export declare function recordHookTiming(hookName: string, durationMs: number, event: string): void;
8
+ export interface TimingStats {
9
+ hook: string;
10
+ count: number;
11
+ p50: number;
12
+ p95: number;
13
+ max: number;
14
+ }
15
+ export declare function getTimingStats(): TimingStats[];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Forgen — Hook Timing Profiler
3
+ *
4
+ * Records hook execution durations and provides timing statistics
5
+ * for visibility into which hooks are slow.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { STATE_DIR } from '../../core/paths.js';
10
+ const TIMING_LOG = path.join(STATE_DIR, 'hook-timing.jsonl');
11
+ const MAX_LINES = 500;
12
+ export function recordHookTiming(hookName, durationMs, event) {
13
+ try {
14
+ fs.mkdirSync(STATE_DIR, { recursive: true });
15
+ const entry = JSON.stringify({ hook: hookName, ms: durationMs, event, at: Date.now() });
16
+ fs.appendFileSync(TIMING_LOG, entry + '\n');
17
+ // Rotate if too large
18
+ try {
19
+ const content = fs.readFileSync(TIMING_LOG, 'utf-8');
20
+ const lines = content.trim().split('\n');
21
+ if (lines.length > MAX_LINES) {
22
+ fs.writeFileSync(TIMING_LOG, lines.slice(-MAX_LINES).join('\n') + '\n');
23
+ }
24
+ }
25
+ catch { /* skip rotation on error */ }
26
+ }
27
+ catch { /* fail-open */ }
28
+ }
29
+ export function getTimingStats() {
30
+ try {
31
+ if (!fs.existsSync(TIMING_LOG))
32
+ return [];
33
+ const content = fs.readFileSync(TIMING_LOG, 'utf-8');
34
+ const entries = content.trim().split('\n')
35
+ .map(line => { try {
36
+ return JSON.parse(line);
37
+ }
38
+ catch {
39
+ return null;
40
+ } })
41
+ .filter(Boolean);
42
+ const byHook = new Map();
43
+ for (const e of entries) {
44
+ if (!byHook.has(e.hook))
45
+ byHook.set(e.hook, []);
46
+ byHook.get(e.hook).push(e.ms);
47
+ }
48
+ const stats = [];
49
+ for (const [hook, times] of byHook) {
50
+ times.sort((a, b) => a - b);
51
+ stats.push({
52
+ hook,
53
+ count: times.length,
54
+ p50: times[Math.floor(times.length * 0.5)] ?? 0,
55
+ p95: times[Math.floor(times.length * 0.95)] ?? 0,
56
+ max: times[times.length - 1] ?? 0,
57
+ });
58
+ }
59
+ return stats.sort((a, b) => b.p95 - a.p95);
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
@@ -5,10 +5,11 @@
5
5
  * Claude Code UserPromptSubmit 훅으로 등록.
6
6
  * 프롬프트와 매칭되는 학습된 스킬을 자동으로 컨텍스트에 주입합니다.
7
7
  *
8
- * 스킬 파일 위치:
8
+ * 스킬 파일 위치 (우선순위):
9
+ * 0. {project}/.forgen/skills/*.md (프로젝트 포지 스킬 — 최우선)
9
10
  * 1. {project}/.compound/skills/*.md (프로젝트 스킬)
10
- * 2. ~/.compound/skills/*.md (글로벌 스킬)
11
- * 3. ~/.compound/me/skills/*.md (개인 학습 스킬)
11
+ * 2. ~/.forgen/me/skills/*.md (개인 학습 스킬)
12
+ * 3. ~/.forgen/skills/*.md (글로벌 스킬)
12
13
  *
13
14
  * 스킬 포맷:
14
15
  * ---
@@ -5,10 +5,11 @@
5
5
  * Claude Code UserPromptSubmit 훅으로 등록.
6
6
  * 프롬프트와 매칭되는 학습된 스킬을 자동으로 컨텍스트에 주입합니다.
7
7
  *
8
- * 스킬 파일 위치:
8
+ * 스킬 파일 위치 (우선순위):
9
+ * 0. {project}/.forgen/skills/*.md (프로젝트 포지 스킬 — 최우선)
9
10
  * 1. {project}/.compound/skills/*.md (프로젝트 스킬)
10
- * 2. ~/.compound/skills/*.md (글로벌 스킬)
11
- * 3. ~/.compound/me/skills/*.md (개인 학습 스킬)
11
+ * 2. ~/.forgen/me/skills/*.md (개인 학습 스킬)
12
+ * 3. ~/.forgen/skills/*.md (글로벌 스킬)
12
13
  *
13
14
  * 스킬 포맷:
14
15
  * ---
@@ -34,10 +35,11 @@ function escapeXmlAttr(s) {
34
35
  return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
35
36
  }
36
37
  import { atomicWriteJSON } from './shared/atomic-write.js';
38
+ import { withFileLockSync } from './shared/file-lock.js';
37
39
  import { FORGEN_HOME, ME_DIR, STATE_DIR } from '../core/paths.js';
38
40
  import { KEYWORD_PATTERNS } from './keyword-detector.js';
39
41
  import { isHookEnabled } from './hook-config.js';
40
- import { approve, approveWithContext, failOpen } from './shared/hook-response.js';
42
+ import { approve, approveWithContext, failOpenWithTracking } from './shared/hook-response.js';
41
43
  /** keyword-detector가 처리하는 키워드 이름 집합 (skill + inject 모두 포함, 이중 주입 방지) */
42
44
  const KEYWORD_DETECTOR_SKILL_NAMES = new Set(KEYWORD_PATTERNS
43
45
  .filter(p => p.type === 'skill' || p.type === 'inject')
@@ -65,11 +67,39 @@ function loadSessionCache(sessionId) {
65
67
  }
66
68
  return new Set();
67
69
  }
68
- function saveSessionCache(sessionId, injected) {
69
- atomicWriteJSON(getSessionCachePath(sessionId), {
70
- injected: [...injected],
71
- updatedAt: new Date().toISOString(),
72
- });
70
+ function saveSessionCache(sessionId, newSkillNames) {
71
+ const cachePath = getSessionCachePath(sessionId);
72
+ try {
73
+ withFileLockSync(cachePath, () => {
74
+ // Lock 안에서 fresh re-read → merge → write
75
+ const freshInjected = new Set();
76
+ try {
77
+ if (fs.existsSync(cachePath)) {
78
+ const fresh = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
79
+ const age = fresh.updatedAt ? Date.now() - new Date(fresh.updatedAt).getTime() : Infinity;
80
+ if (Number.isFinite(age) && age <= 24 * 60 * 60 * 1000) {
81
+ for (const name of fresh.injected ?? [])
82
+ freshInjected.add(name);
83
+ }
84
+ }
85
+ }
86
+ catch { /* fresh re-read 실패 시 빈 set으로 진행 */ }
87
+ for (const name of newSkillNames)
88
+ freshInjected.add(name);
89
+ atomicWriteJSON(cachePath, {
90
+ injected: [...freshInjected],
91
+ updatedAt: new Date().toISOString(),
92
+ });
93
+ });
94
+ }
95
+ catch (e) {
96
+ // Lock 실패 시 fail-open: 직접 write (중복 주입 가능성 있지만 기능 차단 안 함)
97
+ log.debug('skill session cache lock 실패 — fallback write', e);
98
+ atomicWriteJSON(cachePath, {
99
+ injected: [...new Set([...loadSessionCache(sessionId), ...newSkillNames])],
100
+ updatedAt: new Date().toISOString(),
101
+ });
102
+ }
73
103
  }
74
104
  /** YAML frontmatter 파싱 (간단한 구현) */
75
105
  export function parseFrontmatter(content) {
@@ -152,8 +182,9 @@ function collectSkills() {
152
182
  const seen = new Map(); // name → source dir
153
183
  // 패키지 내장 스킬 경로 (dist/../skills/)
154
184
  const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
155
- // v1: 스킬 제거. 프로젝트 > 개인 > 글로벌 > 패키지 내장
185
+ // 프로젝트 .forgen > 프로젝트 .compound > 개인 > 글로벌 > 패키지 내장
156
186
  const dirs = [
187
+ path.join(process.cwd(), '.forgen', 'skills'),
157
188
  path.join(process.cwd(), '.compound', 'skills'),
158
189
  path.join(ME_DIR, 'skills'),
159
190
  path.join(FORGEN_HOME, 'skills'),
@@ -258,11 +289,11 @@ async function main() {
258
289
  }
259
290
  // 최대 제한 적용
260
291
  const toInject = matched.slice(0, MAX_SKILLS_PER_SESSION - injected.size);
261
- // 파일 기반 캐시 업데이트
262
- for (const skill of toInject) {
263
- injected.add(skill.name);
264
- }
265
- saveSessionCache(sessionId, injected);
292
+ // 파일 기반 캐시 업데이트 (lock 보호)
293
+ const newSkillNames = toInject.map(s => s.name);
294
+ for (const name of newSkillNames)
295
+ injected.add(name);
296
+ saveSessionCache(sessionId, newSkillNames);
266
297
  // Adaptive budget: 다른 플러그인 감지 시 스킬 주입량 축소
267
298
  let skillCap = 3000; // INJECTION_CAPS.skillContentMax 기본값
268
299
  try {
@@ -281,5 +312,5 @@ async function main() {
281
312
  }
282
313
  main().catch((e) => {
283
314
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
284
- console.log(failOpen());
315
+ console.log(failOpenWithTracking('skill-injector'));
285
316
  });
@@ -9,7 +9,7 @@
9
9
  import { readStdinJSON } from './shared/read-stdin.js';
10
10
  import { createLogger } from '../core/logger.js';
11
11
  import { isHookEnabled, loadHookConfig } from './hook-config.js';
12
- import { approve, approveWithWarning, failOpen } from './shared/hook-response.js';
12
+ import { approve, approveWithWarning, failOpenWithTracking } from './shared/hook-response.js';
13
13
  const log = createLogger('slop-detector');
14
14
  export const SLOP_PATTERNS = [
15
15
  { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|handle)/i, message: 'Leftover TODO comment', severity: 'warn' },
@@ -84,10 +84,10 @@ async function main() {
84
84
  }
85
85
  catch (e) {
86
86
  log.debug('슬롭 감지 실패', e);
87
- console.log(failOpen());
87
+ console.log(failOpenWithTracking('slop-detector'));
88
88
  }
89
89
  }
90
90
  main().catch((e) => {
91
91
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
92
- console.log(failOpen());
92
+ console.log(failOpenWithTracking('slop-detector'));
93
93
  });