@wooojin/forgen 0.2.1 → 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 (124) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.ko.md +25 -14
  3. package/README.md +61 -17
  4. package/agents/analyst.md +48 -4
  5. package/agents/architect.md +39 -4
  6. package/agents/code-reviewer.md +107 -77
  7. package/agents/critic.md +47 -4
  8. package/agents/debugger.md +46 -4
  9. package/agents/designer.md +40 -4
  10. package/agents/executor.md +112 -30
  11. package/agents/explore.md +45 -5
  12. package/agents/git-master.md +48 -4
  13. package/agents/planner.md +121 -18
  14. package/agents/test-engineer.md +58 -4
  15. package/agents/verifier.md +92 -77
  16. package/commands/architecture-decision.md +127 -258
  17. package/commands/calibrate.md +225 -0
  18. package/commands/code-review.md +163 -178
  19. package/commands/compound.md +127 -68
  20. package/commands/deep-interview.md +212 -110
  21. package/commands/docker.md +68 -178
  22. package/commands/forge-loop.md +215 -0
  23. package/commands/learn.md +231 -0
  24. package/commands/retro.md +215 -0
  25. package/commands/ship.md +277 -0
  26. package/dist/cli.js +17 -9
  27. package/dist/core/auto-compound-runner.js +14 -0
  28. package/dist/core/config-injector.d.ts +2 -1
  29. package/dist/core/config-injector.js +2 -1
  30. package/dist/core/dashboard.d.ts +17 -0
  31. package/dist/core/dashboard.js +112 -2
  32. package/dist/core/harness.d.ts +6 -1
  33. package/dist/core/harness.js +75 -19
  34. package/dist/core/paths.d.ts +6 -1
  35. package/dist/core/paths.js +18 -2
  36. package/dist/core/spawn.d.ts +3 -2
  37. package/dist/core/spawn.js +27 -8
  38. package/dist/core/types.d.ts +34 -0
  39. package/dist/engine/compound-lifecycle.d.ts +4 -3
  40. package/dist/engine/compound-lifecycle.js +91 -46
  41. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  42. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  43. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  44. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  45. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  46. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  47. package/dist/engine/meta-learning/runner.d.ts +14 -0
  48. package/dist/engine/meta-learning/runner.js +90 -0
  49. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  50. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  51. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  52. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  53. package/dist/engine/meta-learning/types.d.ts +114 -0
  54. package/dist/engine/meta-learning/types.js +43 -0
  55. package/dist/engine/solution-format.d.ts +2 -2
  56. package/dist/engine/solution-format.js +249 -34
  57. package/dist/engine/solution-index.d.ts +1 -1
  58. package/dist/engine/solution-matcher.d.ts +7 -1
  59. package/dist/engine/solution-matcher.js +114 -37
  60. package/dist/fgx.js +12 -8
  61. package/dist/hooks/context-guard.d.ts +5 -0
  62. package/dist/hooks/context-guard.js +118 -2
  63. package/dist/hooks/hooks-generator.d.ts +3 -0
  64. package/dist/hooks/hooks-generator.js +23 -6
  65. package/dist/hooks/keyword-detector.js +16 -100
  66. package/dist/hooks/skill-injector.d.ts +4 -3
  67. package/dist/hooks/skill-injector.js +6 -4
  68. package/dist/host/codex-adapter.d.ts +10 -0
  69. package/dist/host/codex-adapter.js +154 -0
  70. package/dist/mcp/solution-reader.d.ts +5 -5
  71. package/dist/mcp/solution-reader.js +34 -24
  72. package/dist/services/session.d.ts +19 -0
  73. package/dist/services/session.js +62 -0
  74. package/hooks/hooks.json +2 -2
  75. package/package.json +2 -1
  76. package/skills/architecture-decision/SKILL.md +113 -257
  77. package/skills/calibrate/SKILL.md +207 -0
  78. package/skills/code-review/SKILL.md +151 -178
  79. package/skills/compound/SKILL.md +126 -68
  80. package/skills/deep-interview/SKILL.md +210 -110
  81. package/skills/docker/SKILL.md +57 -179
  82. package/skills/forge-loop/SKILL.md +198 -0
  83. package/skills/learn/SKILL.md +216 -0
  84. package/skills/retro/SKILL.md +199 -0
  85. package/skills/ship/SKILL.md +259 -0
  86. package/agents/code-simplifier.md +0 -197
  87. package/agents/performance-reviewer.md +0 -172
  88. package/agents/qa-tester.md +0 -158
  89. package/agents/refactoring-expert.md +0 -168
  90. package/agents/scientist.md +0 -144
  91. package/agents/security-reviewer.md +0 -137
  92. package/agents/writer.md +0 -184
  93. package/commands/api-design.md +0 -268
  94. package/commands/ci-cd.md +0 -270
  95. package/commands/database.md +0 -263
  96. package/commands/debug-detective.md +0 -99
  97. package/commands/documentation.md +0 -276
  98. package/commands/ecomode.md +0 -51
  99. package/commands/frontend.md +0 -271
  100. package/commands/git-master.md +0 -90
  101. package/commands/incident-response.md +0 -292
  102. package/commands/migrate.md +0 -101
  103. package/commands/performance.md +0 -288
  104. package/commands/refactor.md +0 -105
  105. package/commands/security-review.md +0 -288
  106. package/commands/specify.md +0 -128
  107. package/commands/tdd.md +0 -183
  108. package/commands/testing-strategy.md +0 -265
  109. package/skills/api-design/SKILL.md +0 -262
  110. package/skills/ci-cd/SKILL.md +0 -264
  111. package/skills/database/SKILL.md +0 -257
  112. package/skills/debug-detective/SKILL.md +0 -95
  113. package/skills/documentation/SKILL.md +0 -270
  114. package/skills/ecomode/SKILL.md +0 -46
  115. package/skills/frontend/SKILL.md +0 -265
  116. package/skills/git-master/SKILL.md +0 -86
  117. package/skills/incident-response/SKILL.md +0 -286
  118. package/skills/migrate/SKILL.md +0 -96
  119. package/skills/performance/SKILL.md +0 -282
  120. package/skills/refactor/SKILL.md +0 -100
  121. package/skills/security-review/SKILL.md +0 -282
  122. package/skills/specify/SKILL.md +0 -122
  123. package/skills/tdd/SKILL.md +0 -178
  124. package/skills/testing-strategy/SKILL.md +0 -260
package/dist/fgx.js CHANGED
@@ -3,14 +3,17 @@
3
3
  * fgx — forgen --dangerously-skip-permissions 의 단축 명령
4
4
  * 모든 인자를 그대로 전달하되, --dangerously-skip-permissions 를 자동 주입
5
5
  */
6
+ import { resolveLaunchContext } from './services/session.js';
7
+ import { prepareHarness, isFirstRun } from './core/harness.js';
8
+ import { spawnClaude } from './core/spawn.js';
6
9
  const args = process.argv.slice(2);
7
10
  // 이미 포함되어 있으면 중복 추가하지 않음
8
- if (!args.includes('--dangerously-skip-permissions')) {
9
- args.unshift('--dangerously-skip-permissions');
11
+ const launchContext = resolveLaunchContext(args);
12
+ const runtime = launchContext.runtime;
13
+ const launchArgs = [...launchContext.args];
14
+ if (!launchArgs.includes('--dangerously-skip-permissions')) {
15
+ launchArgs.unshift('--dangerously-skip-permissions');
10
16
  }
11
- // cli.ts 의 main 로직을 재사용
12
- import { prepareHarness, isFirstRun } from './core/harness.js';
13
- import { spawnClaude } from './core/spawn.js';
14
17
  async function main() {
15
18
  // Security warning — fgx bypasses all Claude Code permission checks
16
19
  console.warn('\n ⚠ fgx: ALL permission checks are disabled (--dangerously-skip-permissions)');
@@ -23,7 +26,7 @@ async function main() {
23
26
  console.log(' Creating ~/.forgen/ directory and default philosophy.');
24
27
  console.log(' Run `forgen onboarding` afterwards to complete personalization.\n');
25
28
  }
26
- const context = await prepareHarness(process.cwd());
29
+ const context = await prepareHarness(process.cwd(), { runtime });
27
30
  if (firstRun) {
28
31
  console.log(' [Done] Initial setup complete.\n');
29
32
  }
@@ -33,8 +36,9 @@ async function main() {
33
36
  console.log(`[forgen] Trust: ${v1.session.effective_trust_policy}`);
34
37
  }
35
38
  console.log('[forgen] Mode: dangerously-skip-permissions');
36
- console.log('[forgen] Starting Claude Code...\n');
37
- await spawnClaude(args, context);
39
+ const runtimeLabel = runtime === 'codex' ? 'Codex' : 'Claude';
40
+ console.log(`[forgen] Starting ${runtimeLabel}...\n`);
41
+ await spawnClaude(launchArgs, context, runtime);
38
42
  }
39
43
  main().catch((err) => {
40
44
  console.error('[forgen] Error:', err instanceof Error ? err.message : err);
@@ -32,3 +32,8 @@ export declare function buildAutoCompactMessage(totalChars: number): string;
32
32
  /** 경고 메시지 생성 (순수 함수) */
33
33
  export declare function buildContextWarningMessage(promptCount: number, totalChars: number): string;
34
34
  export declare function main(): Promise<void>;
35
+ /**
36
+ * forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
37
+ * OMC의 persistent-mode.cjs 패턴 참고.
38
+ */
39
+ export declare function checkForgeLoopActive(): string | null;
@@ -89,6 +89,12 @@ export async function main() {
89
89
  // Stop 훅: stop_hook_type이 있으면 처리
90
90
  if (input.stop_hook_type) {
91
91
  _hookEvent = 'Stop';
92
+ // forge-loop 활성 시 미완료 스토리 감지 → 지속 메시지 주입 (polite-stop 방지)
93
+ const forgeLoopBlock = checkForgeLoopActive();
94
+ if (forgeLoopBlock) {
95
+ console.log(forgeLoopBlock);
96
+ return;
97
+ }
92
98
  // 에러가 포함된 경우: context limit 감지
93
99
  if (input.error) {
94
100
  const errorMsg = input.error;
@@ -119,12 +125,14 @@ export async function main() {
119
125
  fs.writeFileSync(path.join(STATE_DIR, 'pending-compound.json'), JSON.stringify(marker));
120
126
  }
121
127
  catch { /* fail-open: marker write failure is non-critical */ }
122
- console.log(approveWithWarning(`[Forgen] Session with ${state.promptCount} prompts ended. Compound loop will auto-trigger on next session start.`));
128
+ const summary = buildSessionSummary(sessionId, state.promptCount);
129
+ console.log(approveWithWarning(`[Forgen] Session with ${state.promptCount} prompts ended.\n${summary}\nCompound loop will auto-trigger on next session start.`));
123
130
  return;
124
131
  }
125
132
  if (state.promptCount >= 10) {
126
133
  // 10-19 prompts: suggest /compound manually
127
- console.log(approveWithWarning(`[Forgen] 세션에서 ${state.promptCount}개의 프롬프트를 처리했습니다. /compound 를 실행하면 이 세션의 학습 내용을 축적할 수 있습니다.`));
134
+ const summary = buildSessionSummary(sessionId, state.promptCount);
135
+ console.log(approveWithWarning(`[Forgen] 이 세션에서 ${state.promptCount}개의 프롬프트를 처리했습니다.\n${summary}/compound 를 실행하면 이 세션의 학습 내용을 축적할 수 있습니다.`));
128
136
  return;
129
137
  }
130
138
  }
@@ -166,6 +174,114 @@ export async function main() {
166
174
  recordHookTiming('context-guard', Date.now() - _hookStart, _hookEvent);
167
175
  }
168
176
  }
177
+ /**
178
+ * 세션 종료 시 "forgen이 도움이 된 정도"를 요약.
179
+ * solution-cache에서 이번 세션에 주입된 compound 솔루션 수를 집계하여
180
+ * 카운터팩추얼 "forgen 없었으면 ~N분 더 걸렸을 것" 메시지 생성.
181
+ */
182
+ function buildSessionSummary(sessionId, promptCount) {
183
+ try {
184
+ const cachePath = path.join(STATE_DIR, `solution-cache-${sessionId}.json`);
185
+ if (!fs.existsSync(cachePath))
186
+ return '';
187
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
188
+ const injected = Array.isArray(cache.injected) ? cache.injected : [];
189
+ if (injected.length === 0)
190
+ return '';
191
+ // 카운터팩추얼: 주입된 compound 1건당 평균 8분 절약 가정 (하한 추정)
192
+ const savedMins = injected.length * 8;
193
+ const savedStr = savedMins >= 60
194
+ ? `${Math.floor(savedMins / 60)}시간 ${savedMins % 60}분`
195
+ : `${savedMins}분`;
196
+ // 상위 3개 솔루션
197
+ const topNames = injected.slice(0, 3).map(i => `"${i.name}"`).join(', ');
198
+ const moreCount = injected.length - 3;
199
+ const topStr = moreCount > 0 ? `${topNames} 외 ${moreCount}개` : topNames;
200
+ return [
201
+ `\n📊 이번 세션 forgen 효과:`,
202
+ ` 주입된 compound: ${injected.length}건 (${topStr})`,
203
+ ` 추정 절약 시간: ${savedStr} (forgen 없었으면 시행착오 필요)`,
204
+ ` 프롬프트 대비 효율: ${(injected.length / promptCount * 100).toFixed(0)}% 의 대화가 축적된 지식의 도움을 받음\n`,
205
+ ].join('\n');
206
+ }
207
+ catch {
208
+ return '';
209
+ }
210
+ }
211
+ // forge-loop 상태 파일 경로
212
+ const FORGE_LOOP_STATE_PATH = path.join(STATE_DIR, 'forge-loop.json');
213
+ // forge-loop 차단 안전 상한 (무한 루프 방지)
214
+ const FORGE_LOOP_MAX_BLOCKS = 30;
215
+ const FORGE_LOOP_STALE_MS = 2 * 60 * 60 * 1000; // 2시간
216
+ /**
217
+ * forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
218
+ * OMC의 persistent-mode.cjs 패턴 참고.
219
+ */
220
+ export function checkForgeLoopActive() {
221
+ try {
222
+ if (!fs.existsSync(FORGE_LOOP_STATE_PATH))
223
+ return null;
224
+ const state = JSON.parse(fs.readFileSync(FORGE_LOOP_STATE_PATH, 'utf-8'));
225
+ if (!state.active)
226
+ return null;
227
+ // Stale 감지: 2시간+ 미활동 → 자동 비활성화
228
+ const startedAt = new Date(state.startedAt).getTime();
229
+ if (Number.isFinite(startedAt) && Date.now() - startedAt > FORGE_LOOP_STALE_MS) {
230
+ state.active = false;
231
+ atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
232
+ return null;
233
+ }
234
+ // 확인 대기 중이면 차단하지 않음 (사용자 개입 허용)
235
+ if (state.awaitingConfirmation)
236
+ return null;
237
+ // 안전 상한: 30회 이상 차단 시 무한 루프로 간주하여 해제
238
+ const blockCount = state.blockCount ?? 0;
239
+ if (blockCount >= FORGE_LOOP_MAX_BLOCKS) {
240
+ state.active = false;
241
+ atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
242
+ return null;
243
+ }
244
+ // 미완료 스토리 확인
245
+ const stories = Array.isArray(state.stories) ? state.stories : [];
246
+ const pending = stories.filter((s) => !s.passes);
247
+ if (pending.length === 0) {
248
+ // 모든 스토리 완료 → forge-loop 종료
249
+ state.active = false;
250
+ atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
251
+ return null;
252
+ }
253
+ // 차단 카운트 증가 + 지속 메시지 주입
254
+ state.blockCount = blockCount + 1;
255
+ state.lastBlockAt = new Date().toISOString();
256
+ atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
257
+ const nextStory = pending[0];
258
+ const message = [
259
+ `<forgen-forge-loop iteration="${state.blockCount}/${FORGE_LOOP_MAX_BLOCKS}">`,
260
+ `[FORGE-LOOP] ${pending.length}개 스토리가 미완료입니다.`,
261
+ `현재 스토리: ${nextStory.id} — ${nextStory.title}`,
262
+ ``,
263
+ `계속 진행하세요. 보고는 다음 시점에만 합니다:`,
264
+ ` 1. 모든 스토리 완료 (최종 리포트)`,
265
+ ` 2. 3회 실패 (에스컬레이션)`,
266
+ ` 3. Context limit 접근 (handoff)`,
267
+ ``,
268
+ `중간 "완료했습니다" 보고는 polite-stop anti-pattern입니다.`,
269
+ `취소하려면: "/forge-loop cancel" 또는 "cancelforgen" 입력`,
270
+ `</forgen-forge-loop>`,
271
+ ].join('\n');
272
+ // block 결정으로 Claude가 계속 작업하도록 강제
273
+ return JSON.stringify({
274
+ continue: true,
275
+ decision: 'block',
276
+ reason: message,
277
+ });
278
+ }
279
+ catch (e) {
280
+ // fail-open: forge-loop 상태 읽기 실패는 차단하지 않음
281
+ log.debug('forge-loop 상태 확인 실패', e);
282
+ return null;
283
+ }
284
+ }
169
285
  function saveHandoff(sessionId, reason, detail) {
170
286
  fs.mkdirSync(HANDOFFS_DIR, { recursive: true });
171
287
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
@@ -9,6 +9,7 @@
9
9
  * - forgen config hooks (사용자 설정 변경 후)
10
10
  * - forgen install (플러그인 설치 후)
11
11
  */
12
+ import { type RuntimeHost } from '../core/types.js';
12
13
  interface HookCommand {
13
14
  type: 'command';
14
15
  command: string;
@@ -27,6 +28,8 @@ interface GenerateOptions {
27
28
  cwd?: string;
28
29
  /** 훅 실행 스크립트의 루트 경로 */
29
30
  pluginRoot?: string;
31
+ /** 런타임 (claude|codex) */
32
+ runtime?: RuntimeHost;
30
33
  }
31
34
  /**
32
35
  * 활성 훅만 포함한 hooks.json 객체를 생성합니다.
@@ -14,6 +14,27 @@ import * as path from 'node:path';
14
14
  import { HOOK_REGISTRY } from './hook-registry.js';
15
15
  import { isHookEnabled } from './hook-config.js';
16
16
  import { detectInstalledPlugins, getHookConflicts } from '../core/plugin-detector.js';
17
+ function splitCommand(raw) {
18
+ const tokens = raw.match(/"([^"]+)"|\S+/g) ?? [];
19
+ const unquoted = tokens.map(token => token.replace(/^"/, '').replace(/"$/, ''));
20
+ return { script: unquoted[0] ?? '', args: unquoted.slice(1) };
21
+ }
22
+ function quoteArg(raw) {
23
+ return `"${raw.replace(/"/g, '\\"')}"`;
24
+ }
25
+ function buildHookCommand(pluginRoot, rawScript, runtime) {
26
+ const { script, args } = splitCommand(rawScript);
27
+ const scriptPath = `${pluginRoot}/${script}`;
28
+ const quotedArgs = args.map(quoteArg).join(' ');
29
+ if (runtime === 'codex') {
30
+ const adapterPath = `${pluginRoot}/host/codex-adapter.js`;
31
+ const baseCommand = `node ${quoteArg(adapterPath)} ${quoteArg(scriptPath)}`;
32
+ return `${baseCommand}${quotedArgs ? ` ${quotedArgs}` : ''}`;
33
+ }
34
+ return quotedArgs
35
+ ? `node ${quoteArg(scriptPath)} ${quotedArgs}`
36
+ : `node ${quoteArg(scriptPath)}`;
37
+ }
17
38
  /**
18
39
  * 활성 훅만 포함한 hooks.json 객체를 생성합니다.
19
40
  *
@@ -27,6 +48,7 @@ export function generateHooksJson(options) {
27
48
  const cwd = options?.cwd;
28
49
  // biome-ignore lint/suspicious/noTemplateCurlyInString: CLAUDE_PLUGIN_ROOT is a Claude Code Plugin SDK variable resolved at runtime
29
50
  const pluginRoot = options?.pluginRoot ?? '${CLAUDE_PLUGIN_ROOT}/dist';
51
+ const runtime = options?.runtime ?? 'claude';
30
52
  // 다른 플러그인의 충돌 훅 감지
31
53
  const hookConflicts = getHookConflicts(cwd);
32
54
  const detectedPlugins = detectInstalledPlugins(cwd);
@@ -64,12 +86,7 @@ export function generateHooksJson(options) {
64
86
  hooks[event] = [...byMatcher.entries()].map(([matcher, matcherEntries]) => ({
65
87
  matcher,
66
88
  hooks: matcherEntries.map(h => {
67
- // script에 인자가 포함된 경우 (예: "hooks/subagent-tracker.js start")
68
- // 파일 경로와 인자를 분리해야 셸에서 ENOENT를 방지
69
- const spaceIdx = h.script.indexOf(' ');
70
- const command = spaceIdx === -1
71
- ? `node "${pluginRoot}/${h.script}"`
72
- : `node "${pluginRoot}/${h.script.slice(0, spaceIdx)}" ${h.script.slice(spaceIdx + 1)}`;
89
+ const command = buildHookCommand(pluginRoot, h.script, runtime);
73
90
  return { type: 'command', command, timeout: h.timeout };
74
91
  }),
75
92
  }));
@@ -29,7 +29,7 @@ import { recordHookTiming } from './shared/hook-timing.js';
29
29
  function escapeXmlAttr(s) {
30
30
  return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
31
31
  }
32
- const WORKFLOW_TRACKED_INJECTS = new Set(['benchmark']);
32
+ const WORKFLOW_TRACKED_INJECTS = new Set();
33
33
  export function shouldTrackWorkflowActivation(match) {
34
34
  if (match.type === 'inject')
35
35
  return WORKFLOW_TRACKED_INJECTS.has(match.keyword);
@@ -51,21 +51,17 @@ export const KEYWORD_PATTERNS = [
51
51
  { pattern: /\bccg\b/i, keyword: 'ccg', type: 'skill', skill: 'ccg' },
52
52
  { pattern: /\bralplan\b/i, keyword: 'ralplan', type: 'skill', skill: 'ralplan' },
53
53
  { pattern: /\bdeep[- ]?interview\b/i, keyword: 'deep-interview', type: 'skill', skill: 'deep-interview' },
54
- { pattern: /\bspecify\b|(?:^|\s)(명세|요구사항\s*정리|스펙\s*정리)(?:\s|$)/im, keyword: 'specify', type: 'skill', skill: 'specify' },
55
54
  { pattern: /\bpipeline\b/i, keyword: 'pipeline', type: 'skill', skill: 'pipeline' },
56
- { pattern: /\becomode\b|(?:^|\s)(에코\s*모드|토큰\s*절약)(?:\s|$)/im, keyword: 'ecomode', type: 'skill', skill: 'ecomode' },
57
55
  // 인젝션 모드
58
56
  { pattern: /\bultrathink\b/i, keyword: 'ultrathink', type: 'inject' },
59
57
  { pattern: /\bdeepsearch\b/i, keyword: 'deepsearch', type: 'inject' },
60
- { pattern: /(?:^|\s)tdd(?:\s+(?:모드|mode|방식|으로|해|해줘|시작|적용)|\s*$)/im, keyword: 'tdd', type: 'skill', skill: 'tdd' },
61
58
  { pattern: /(?:code[- ]?review|코드\s*리뷰)\s*(?:해|해줘|시작|해봐|부탁|mode|모드)/i, keyword: 'code-review', type: 'skill', skill: 'code-review' },
62
- { pattern: /(?:security[- ]?review|보안\s*리뷰|보안\s*검토)\s*(?:해|해줘|시작|해봐|부탁|mode|모드)/i, keyword: 'security-review', type: 'skill', skill: 'security-review' },
63
- // 실용 스킬 명시적 모드 호출만 매칭 (일상 단어 false positive 방지)
64
- { pattern: /\bgit[- ]?master\b/i, keyword: 'git-master', type: 'skill', skill: 'git-master' },
65
- { pattern: /\b(benchmark)\s*(?:mode|모드|해|해줘|시작|실행|돌려)|(?:^|\s)(벤치마크)\s*(?:mode|모드|해|해줘|시작|실행|돌려)|(?:^|\s)성능\s*측정/im, keyword: 'benchmark', type: 'inject' },
66
- { pattern: /\b(migrate)\s*(?:mode|모드|해|해줘|시작|실행|진행)|(?:^|\s)(마이그레이션)\s*(?:mode|모드|해|해줘|시작|실행|진행)/im, keyword: 'migrate', type: 'skill', skill: 'migrate' },
67
- { pattern: /\b(debug[- ]?detective)\b|(?:^|\s)(디버그\s*탐정|체계적\s*디버깅)(?:\s|$)/im, keyword: 'debug-detective', type: 'skill', skill: 'debug-detective' },
68
- { pattern: /\b(refactor)\s*(?:mode|모드|해|해줘|시작|실행|진행)|(?:^|\s)(리팩토링|리팩터)\s*(?:mode|모드|해|해줘|시작|실행|진행)/im, keyword: 'refactor', type: 'skill', skill: 'refactor' },
59
+ // forgen 핵심 스킬
60
+ { pattern: /\b(forge[- ]?loop|포지[- ]?루프)\b|(?:^|\s)(끝까지|don'?t\s*stop)(?:\s|$)/im, keyword: 'forge-loop', type: 'skill', skill: 'forge-loop' },
61
+ { pattern: /(?:^|\s)ship(?:\s|$)|(?:^|\s)(배포|릴리스)\s*(?:해|해줘|하자|시작|진행)/im, keyword: 'ship', type: 'skill', skill: 'ship' },
62
+ { pattern: /\bretro\b|(?:^|\s)(회고|돌아보기)(?:\s|$)/im, keyword: 'retro', type: 'skill', skill: 'retro' },
63
+ { pattern: /(?:^|\s)learn\s+(?:search|prune|stats|export)|(?:^|\s)(학습\s*관리|compound\s*정리|솔루션\s*정리)/im, keyword: 'learn', type: 'skill', skill: 'learn' },
64
+ { pattern: /\bcalibrate\b|(?:^|\s)(캘리브|프로필\s*보정|프로필\s*조정|프로필\s*확인)(?:\s|$)/im, keyword: 'calibrate', type: 'skill', skill: 'calibrate' },
69
65
  ];
70
66
  // ── 인젝션 메시지 ──
71
67
  const INJECT_MESSAGES = {
@@ -84,98 +80,12 @@ Perform comprehensive codebase exploration before answering:
84
80
  4. Cross-reference findings across files
85
81
  5. Present a complete, evidence-based analysis
86
82
  </compound-deepsearch>`,
87
- tdd: `<compound-tdd>
88
- TDD MODE ACTIVATED.
89
- Follow strict Test-Driven Development:
90
- 1. Write the failing test FIRST (Red)
91
- 2. Write the minimum code to pass (Green)
92
- 3. Refactor while keeping tests green (Refactor)
93
- 4. Repeat for each requirement
94
- Never write implementation before tests.
95
- </compound-tdd>`,
96
- 'code-review': `<compound-code-review>
97
- CODE REVIEW MODE ACTIVATED.
98
- Perform thorough code review with severity ratings:
99
- - 🔴 CRITICAL: Security vulnerabilities, data loss risks, crashes
100
- - 🟡 MAJOR: Logic errors, performance issues, missing error handling
101
- - 🔵 MINOR: Style, naming, documentation improvements
102
- - 💡 SUGGESTION: Optional enhancements
103
- Provide file:line references for every finding.
104
- </compound-code-review>`,
105
- 'security-review': `<compound-security-review>
106
- SECURITY REVIEW MODE ACTIVATED.
107
- Check for OWASP Top 10 and common vulnerabilities:
108
- 1. Injection (SQL, XSS, Command)
109
- 2. Broken Authentication / Authorization
110
- 3. Sensitive Data Exposure
111
- 4. Security Misconfiguration
112
- 5. Insecure Dependencies
113
- 6. Secrets in code (API keys, tokens, passwords)
114
- 7. Input validation gaps
115
- 8. Unsafe deserialization
116
- Rate each finding: CRITICAL / HIGH / MEDIUM / LOW
117
- </compound-security-review>`,
118
- 'git-master': `<compound-git-master>
119
- GIT MASTER MODE ACTIVATED.
120
- Apply atomic commit strategy and clean history management:
121
- 1. One commit = one logical change (atomic)
122
- 2. Follow Conventional Commits: feat/fix/refactor/docs/chore(<scope>): <subject>
123
- 3. Use interactive rebase (git rebase -i) to clean up WIP commits before pushing
124
- 4. Never force-push to shared branches (main, develop)
125
- 5. Use git bisect for systematic bug hunt across commits
126
- Commit message format: <type>(<scope>): <subject> — imperative, 50 chars max
127
- </compound-git-master>`,
128
- benchmark: `<compound-benchmark>
129
- BENCHMARK MODE ACTIVATED.
130
- Measure performance with statistical rigor:
131
- 1. Collect baseline metrics FIRST (before any changes)
132
- 2. Run minimum 30 iterations (skip first 5 as warmup)
133
- 3. Calculate: avg, p95, p99, min, max
134
- 4. Measure: execution time (performance.now()), memory (process.memoryUsage()), bundle size
135
- 5. Output before/after comparison table with delta percentages
136
- 6. Use same environment for both measurements to ensure validity
137
- </compound-benchmark>`,
138
- migrate: `<compound-migrate>
139
- MIGRATION MODE ACTIVATED.
140
- Follow the 5-phase safe migration workflow:
141
- 1. ANALYZE: Document current state, identify breaking changes, map affected files
142
- 2. PLAN: Decompose into atomic steps, define rollback triggers (error rate > N%)
143
- 3. BACKUP: Create DB dump + git tag as restore point before any changes
144
- 4. EXECUTE: Apply Expand-Contract pattern for zero-downtime DB changes
145
- 5. VERIFY: Run E2E tests, check data integrity, validate performance regression
146
- Rollback criteria: error rate spike, latency > 2x baseline, data inconsistency
147
- </compound-migrate>`,
148
- 'debug-detective': `<compound-debug-detective>
149
- DEBUG DETECTIVE MODE ACTIVATED.
150
- Follow the Reproduce → Isolate → Fix → Verify loop:
151
- 1. REPRODUCE: Document exact conditions, input, expected vs actual, reproduction rate
152
- 2. ISOLATE: Classify error type (runtime/type/logic/async), use git bisect for regression
153
- 3. FIX: Address root cause (not symptoms), minimize change scope
154
- 4. VERIFY: Add regression test, confirm fix in staging before production
155
- Error classification:
156
- - Runtime: TypeError/ReferenceError → trace stack
157
- - Logic: wrong output → add intermediate logging
158
- - Async: race condition → check Promise chain, event ordering
159
- Never guess — always reproduce first.
160
- </compound-debug-detective>`,
161
- refactor: `<compound-refactor>
162
- REFACTOR MODE ACTIVATED.
163
- Safe refactoring with test-first approach:
164
- 1. SECURE TESTS: Characterization tests for untested code before touching anything
165
- 2. IDENTIFY SMELLS: Long functions (>50 lines), duplication, deep nesting (>3), magic numbers
166
- 3. APPLY SOLID: Single responsibility, Open-closed, Liskov, Interface segregation, Dependency inversion
167
- 4. REFACTOR CATALOG: Extract Method, Move Method, Replace Conditional with Polymorphism
168
- 5. VERIFY: Run full test suite after each refactoring step
169
- Rules:
170
- - Never mix refactoring + feature changes in the same commit
171
- - One refactoring pattern per commit
172
- - Keep tests green at all times
173
- </compound-refactor>`,
174
83
  };
175
84
  // ── 스킬 파일 로드 ──
176
85
  function loadSkillContent(skillName) {
177
- // 스킬 파일 검색 순서: 프로젝트 > 연결된 > 글로벌 팩 > 글로벌 > 패키지 내장
86
+ // 스킬 파일 검색 순서: 프로젝트 .forgen > 프로젝트 .compound > 팩 > 개인 > 글로벌 > 패키지 내장
178
87
  const searchPaths = [
88
+ path.join(process.cwd(), '.forgen', 'skills', `${skillName}.md`),
179
89
  path.join(process.cwd(), '.compound', 'skills', `${skillName}.md`),
180
90
  path.join(process.cwd(), 'skills', `${skillName}.md`),
181
91
  ];
@@ -302,7 +212,7 @@ async function main() {
302
212
  catch { /* 파일 없으면 무시 */ }
303
213
  }
304
214
  else {
305
- // 모든 모드 상태 초기화 (ralplan, deep-interview 포함)
215
+ // 모든 모드 상태 초기화 (ralplan, deep-interview, forge-loop 등 포함)
306
216
  for (const mode of ALL_MODES) {
307
217
  clearState(`${mode}-state`);
308
218
  }
@@ -311,6 +221,12 @@ async function main() {
311
221
  fs.unlinkSync(ralphLoopState);
312
222
  }
313
223
  catch { /* 파일 없으면 무시 */ }
224
+ // forge-loop 상태 파일도 명시적으로 삭제 (Stop 훅 차단 해제)
225
+ const forgeLoopState = path.join(STATE_DIR, 'forge-loop.json');
226
+ try {
227
+ fs.unlinkSync(forgeLoopState);
228
+ }
229
+ catch { /* 파일 없으면 무시 */ }
314
230
  }
315
231
  // skill-cache 파일도 정리 (재주입 가능하도록)
316
232
  cleanSkillCaches();
@@ -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
  * ---
@@ -181,8 +182,9 @@ function collectSkills() {
181
182
  const seen = new Map(); // name → source dir
182
183
  // 패키지 내장 스킬 경로 (dist/../skills/)
183
184
  const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
184
- // v1: 스킬 제거. 프로젝트 > 개인 > 글로벌 > 패키지 내장
185
+ // 프로젝트 .forgen > 프로젝트 .compound > 개인 > 글로벌 > 패키지 내장
185
186
  const dirs = [
187
+ path.join(process.cwd(), '.forgen', 'skills'),
186
188
  path.join(process.cwd(), '.compound', 'skills'),
187
189
  path.join(ME_DIR, 'skills'),
188
190
  path.join(FORGEN_HOME, 'skills'),
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Codex 훅 어댑터
4
+ *
5
+ * 목적:
6
+ * - codex 런타임에서 실행되는 훅 스크립트 출력을 Claude Hook schema로 정규화
7
+ * - continue 누락 또는 codex 특화 판정 필드(approved/decision) 대응
8
+ * - 파싱 실패/실행 실패 시 fail-open(continue: true)
9
+ */
10
+ export {};
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Codex 훅 어댑터
4
+ *
5
+ * 목적:
6
+ * - codex 런타임에서 실행되는 훅 스크립트 출력을 Claude Hook schema로 정규화
7
+ * - continue 누락 또는 codex 특화 판정 필드(approved/decision) 대응
8
+ * - 파싱 실패/실행 실패 시 fail-open(continue: true)
9
+ */
10
+ import { spawnSync } from 'node:child_process';
11
+ function parseDecision(raw) {
12
+ if (typeof raw === 'boolean') {
13
+ return { continueFlag: raw };
14
+ }
15
+ if (typeof raw === 'string') {
16
+ const normalized = raw.toLowerCase();
17
+ if (normalized === 'continue')
18
+ return { continueFlag: true };
19
+ if (normalized === 'stop' || normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
20
+ return { continueFlag: false, permissionDecision: normalized };
21
+ }
22
+ return { continueFlag: true };
23
+ }
24
+ if (typeof raw !== 'object' || raw === null)
25
+ return { continueFlag: true };
26
+ const value = raw.decision;
27
+ if (typeof value === 'string') {
28
+ const normalized = value.toLowerCase();
29
+ if (normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
30
+ return { continueFlag: false, permissionDecision: normalized };
31
+ }
32
+ if (normalized === 'ask' || normalized === 'prompt' || normalized === 'confirm') {
33
+ return { continueFlag: true, permissionDecision: normalized };
34
+ }
35
+ }
36
+ if (typeof raw.approved === 'boolean') {
37
+ const approved = raw.approved;
38
+ return approved
39
+ ? { continueFlag: true, permissionDecision: raw.decision || 'approve' }
40
+ : { continueFlag: false, permissionDecision: 'deny' };
41
+ }
42
+ if (typeof raw.continue === 'boolean') {
43
+ return { continueFlag: raw.continue };
44
+ }
45
+ return { continueFlag: true };
46
+ }
47
+ function lastJSONObjectFromText(raw) {
48
+ const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
49
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
50
+ try {
51
+ return JSON.parse(lines[i]);
52
+ }
53
+ catch {
54
+ // continue
55
+ }
56
+ }
57
+ try {
58
+ return JSON.parse(raw);
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function normalizeOutput(raw, input) {
65
+ const result = { continue: true };
66
+ const decision = parseDecision(raw);
67
+ result.continue = decision.continueFlag;
68
+ if (typeof raw === 'object' && raw !== null) {
69
+ const payload = raw;
70
+ if (typeof payload.continue === 'boolean')
71
+ result.continue = payload.continue;
72
+ if (typeof payload.systemMessage === 'string')
73
+ result.systemMessage = payload.systemMessage;
74
+ if (typeof payload.suppressOutput === 'boolean')
75
+ result.suppressOutput = payload.suppressOutput;
76
+ if (typeof payload.hookSpecificOutput === 'object' && payload.hookSpecificOutput !== null) {
77
+ result.hookSpecificOutput = { ...payload.hookSpecificOutput };
78
+ }
79
+ if (typeof payload.decision === 'string') {
80
+ result.hookSpecificOutput = {
81
+ ...(result.hookSpecificOutput ?? {}),
82
+ permissionDecision: payload.decision,
83
+ };
84
+ }
85
+ }
86
+ const eventName = result.hookSpecificOutput?.hookEventName ?? input.hookEventName ?? input.event;
87
+ if (eventName) {
88
+ result.hookSpecificOutput = {
89
+ hookEventName: eventName,
90
+ ...(result.hookSpecificOutput ?? {}),
91
+ };
92
+ }
93
+ if (!result.continue && !result.hookSpecificOutput?.permissionDecision) {
94
+ if (decision.permissionDecision)
95
+ result.hookSpecificOutput = {
96
+ ...(result.hookSpecificOutput ?? {}),
97
+ permissionDecision: decision.permissionDecision,
98
+ };
99
+ else
100
+ result.hookSpecificOutput = { ...(result.hookSpecificOutput ?? {}), permissionDecision: 'deny' };
101
+ }
102
+ return result;
103
+ }
104
+ async function main() {
105
+ const [delegatePath, ...restArgs] = process.argv.slice(2);
106
+ if (!delegatePath) {
107
+ console.log(JSON.stringify({ continue: true }));
108
+ return;
109
+ }
110
+ const input = await (async () => {
111
+ const chunks = [];
112
+ let totalBytes = 0;
113
+ for await (const chunk of process.stdin) {
114
+ chunks.push(chunk);
115
+ totalBytes += chunk.length;
116
+ if (totalBytes > 10 * 1024 * 1024)
117
+ break;
118
+ }
119
+ const raw = Buffer.concat(chunks.map(c => typeof c === 'string' ? Buffer.from(c) : c)).toString('utf-8').trim();
120
+ if (!raw)
121
+ return {};
122
+ try {
123
+ return JSON.parse(raw);
124
+ }
125
+ catch {
126
+ return {};
127
+ }
128
+ })();
129
+ try {
130
+ const result = spawnSync(process.execPath, [delegatePath, ...restArgs], {
131
+ encoding: 'utf-8',
132
+ input: JSON.stringify(input),
133
+ cwd: process.cwd(),
134
+ stdio: ['pipe', 'pipe', 'pipe'],
135
+ });
136
+ if (result.error) {
137
+ console.log(JSON.stringify({ continue: true }));
138
+ return;
139
+ }
140
+ const parsed = lastJSONObjectFromText(result.stdout ?? '');
141
+ if (!parsed) {
142
+ console.log(JSON.stringify({ continue: true }));
143
+ return;
144
+ }
145
+ const output = normalizeOutput(parsed, input);
146
+ console.log(JSON.stringify(output));
147
+ }
148
+ catch {
149
+ console.log(JSON.stringify({ continue: true }));
150
+ }
151
+ }
152
+ main().catch(() => {
153
+ console.log(JSON.stringify({ continue: true }));
154
+ });