@wooojin/forgen 0.4.8 → 0.4.9

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 (122) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/assets/dev-guide/be/README.md +226 -0
  3. package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
  4. package/assets/dev-guide/be/principles/common.md +433 -0
  5. package/assets/dev-guide/be/principles/go.md +469 -0
  6. package/assets/dev-guide/be/principles/node.md +388 -0
  7. package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
  8. package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
  9. package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
  10. package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
  11. package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
  12. package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
  13. package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
  14. package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
  15. package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
  16. package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
  17. package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
  18. package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
  19. package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
  20. package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
  21. package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
  22. package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
  23. package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
  24. package/assets/dev-guide/fe/README.md +197 -0
  25. package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
  26. package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
  27. package/assets/dev-guide/fe/principles/common.md +160 -0
  28. package/assets/dev-guide/fe/principles/react.md +183 -0
  29. package/assets/dev-guide/fe/principles/vue.md +196 -0
  30. package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
  31. package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
  32. package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
  33. package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
  34. package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
  35. package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
  36. package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
  37. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
  38. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
  39. package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
  40. package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
  41. package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
  42. package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
  43. package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
  44. package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
  45. package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
  46. package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
  47. package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
  48. package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
  49. package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
  50. package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
  51. package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
  52. package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
  53. package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
  54. package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
  55. package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
  56. package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
  57. package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
  58. package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
  59. package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
  60. package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
  61. package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
  62. package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
  63. package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
  64. package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
  65. package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
  66. package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
  67. package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
  68. package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
  69. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
  70. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
  71. package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
  72. package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
  73. package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
  74. package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
  75. package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
  76. package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
  77. package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
  78. package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
  79. package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
  80. package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
  81. package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
  82. package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
  83. package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
  84. package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
  85. package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
  86. package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
  87. package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
  88. package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
  89. package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
  90. package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
  91. package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
  92. package/dist/cli.js +42 -0
  93. package/dist/core/dashboard-cli.d.ts +12 -0
  94. package/dist/core/dashboard-cli.js +226 -0
  95. package/dist/core/dev-guide-injector.d.ts +26 -0
  96. package/dist/core/dev-guide-injector.js +137 -0
  97. package/dist/core/init.js +53 -0
  98. package/dist/core/lifecycle-classifier.d.ts +23 -0
  99. package/dist/core/lifecycle-classifier.js +104 -0
  100. package/dist/core/observability-backfill.d.ts +31 -0
  101. package/dist/core/observability-backfill.js +178 -0
  102. package/dist/core/observability-store.d.ts +58 -0
  103. package/dist/core/observability-store.js +195 -0
  104. package/dist/core/session-store.js +4 -0
  105. package/dist/core/spawn.d.ts +17 -0
  106. package/dist/core/spawn.js +179 -2
  107. package/dist/core/statusline-cli.js +34 -1
  108. package/dist/engine/compound-extractor.js +39 -0
  109. package/dist/engine/compound-loop.js +6 -0
  110. package/dist/engine/compound-retire.d.ts +20 -0
  111. package/dist/engine/compound-retire.js +85 -0
  112. package/dist/hooks/context-guard.js +25 -1
  113. package/dist/hooks/post-tool-use.js +48 -0
  114. package/dist/hooks/solution-injector.js +93 -0
  115. package/dist/host/install-claude.d.ts +6 -2
  116. package/dist/host/install-claude.js +74 -2
  117. package/dist/host/install-codex.d.ts +4 -0
  118. package/dist/host/install-codex.js +71 -0
  119. package/dist/host/install-orchestrator.js +1 -0
  120. package/package.json +6 -6
  121. package/plugin.json +1 -1
  122. package/scripts/postinstall.js +134 -0
@@ -30,6 +30,9 @@ import { STATE_DIR } from '../core/paths.js';
30
30
  import { recordHookTiming } from './shared/hook-timing.js';
31
31
  import { appendPending, flushAccept } from '../engine/solution-outcomes.js';
32
32
  import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
33
+ import { emitSolutionEvent, querySurfacedWithin } from '../core/observability-store.js';
34
+ import { parseSolutionV3 } from '../engine/solution-format.js';
35
+ import { ME_SOLUTIONS } from '../core/paths.js';
33
36
  const MAX_SOLUTIONS_PER_SESSION = 10;
34
37
  /**
35
38
  * Minimum relevance thresholds by fitness state (2026-04-21 gate sweep).
@@ -252,6 +255,56 @@ function backfillCacheTagsOnDisk(cachePath, allMatched) {
252
255
  }
253
256
  }
254
257
  }
258
+ /**
259
+ * 직전 5분 내 surfaced 된 솔루션이 현재 프롬프트와 키워드 매칭 시 acted_on emit.
260
+ * tags/identifiers 가 없는 솔루션은 skip. fail-open.
261
+ */
262
+ async function detectActOnFromPriorSurface(sessionId, promptLower) {
263
+ try {
264
+ const recentSurfaces = querySurfacedWithin(sessionId, 5);
265
+ if (recentSurfaces.length === 0)
266
+ return;
267
+ const seen = new Set();
268
+ for (const surf of recentSurfaces) {
269
+ if (seen.has(surf.solutionId))
270
+ continue;
271
+ seen.add(surf.solutionId);
272
+ // 솔루션 파일 로드 (ME_SOLUTIONS 기준)
273
+ const filePath = path.join(ME_SOLUTIONS, `${surf.solutionId}.md`);
274
+ if (!fs.existsSync(filePath))
275
+ continue;
276
+ let raw;
277
+ try {
278
+ raw = fs.readFileSync(filePath, 'utf-8');
279
+ }
280
+ catch {
281
+ continue;
282
+ }
283
+ const sol = parseSolutionV3(raw);
284
+ if (!sol)
285
+ continue;
286
+ const tags = sol.frontmatter.tags ?? [];
287
+ const identifiers = sol.frontmatter.identifiers ?? [];
288
+ if (tags.length === 0 && identifiers.length === 0)
289
+ continue;
290
+ const hit = tags.some(t => promptLower.includes(t.toLowerCase()))
291
+ || identifiers.some(id => promptLower.includes(id.toLowerCase()));
292
+ if (!hit)
293
+ continue;
294
+ emitSolutionEvent({
295
+ sessionId,
296
+ solutionId: surf.solutionId,
297
+ eventType: 'acted_on',
298
+ signalSource: 'prompt-keyword',
299
+ signalScore: 0.20,
300
+ meta: { surface_ts: surf.ts },
301
+ });
302
+ }
303
+ }
304
+ catch (e) {
305
+ log.debug('detectActOnFromPriorSurface 실패', e);
306
+ }
307
+ }
255
308
  async function main() {
256
309
  const _hookStart = Date.now();
257
310
  try {
@@ -265,6 +318,8 @@ async function main() {
265
318
  return;
266
319
  }
267
320
  const sessionId = input.session_id ?? 'default';
321
+ // Observability P2: 직전 surfaced 솔루션과 현재 프롬프트 키워드 매칭 → acted_on emit
322
+ await detectActOnFromPriorSurface(sessionId, input.prompt.toLowerCase());
268
323
  // v1: 교정 감지 → correction-record 호출 유도 hint
269
324
  const correctionPatterns = /하지\s*마|그렇게\s*말고|앞으로는|이렇게\s*해|stop\s+doing|don'?t\s+do|always\s+do|never\s+do|아니\s*그게\s*아니라/i;
270
325
  if (correctionPatterns.test(input.prompt)) {
@@ -294,6 +349,25 @@ async function main() {
294
349
  // 다시 매칭되면 그 정보로 cache의 missing tags를 채울 수 있다.
295
350
  // matches는 새 주입 후보 (이미 injected는 제외).
296
351
  const allMatched = matchSolutions(input.prompt, scope, cwd);
352
+ // Observability P1: matched emit — top-5 후보 각각 기록
353
+ try {
354
+ for (const candidate of allMatched.slice(0, 5)) {
355
+ emitSolutionEvent({
356
+ sessionId,
357
+ solutionId: candidate.name,
358
+ eventType: 'matched',
359
+ signalSource: 'matcher',
360
+ signalScore: candidate.relevance,
361
+ meta: {
362
+ matchedTags: candidate.matchedTags,
363
+ matchedIdentifiers: candidate.matchedIdentifiers,
364
+ },
365
+ });
366
+ }
367
+ }
368
+ catch (e) {
369
+ log.debug('matched emit 실패', e);
370
+ }
297
371
  const matches = allMatched.filter(m => !injected.has(m.name));
298
372
  // T3: emit a ranking-decision record for offline analysis. Fail-open —
299
373
  // the logger swallows any error so this never blocks hook approval.
@@ -566,6 +640,25 @@ async function main() {
566
640
  catch (e) {
567
641
  log.debug('recommendation_surfaced emit 실패', e);
568
642
  }
643
+ // Observability P1: surfaced emit — approveWithContext 직전 effectiveToInject 각각 기록
644
+ try {
645
+ for (const sol of effectiveToInject) {
646
+ emitSolutionEvent({
647
+ sessionId,
648
+ solutionId: sol.name,
649
+ eventType: 'surfaced',
650
+ signalSource: 'hook-prepend',
651
+ signalScore: sol.relevance,
652
+ meta: {
653
+ surfaceChars: fullInjection.length,
654
+ injectionMode: 'context-prepend',
655
+ },
656
+ });
657
+ }
658
+ }
659
+ catch (e) {
660
+ log.debug('surfaced emit 실패', e);
661
+ }
569
662
  // H1: 사용자 UI 에 recall hit 1줄 노출. additionalContext 는 모델 전용이라
570
663
  // v0.4.0 에서 8,000+ 주입이 발생했는데도 사용자는 0건을 봤다. systemMessage
571
664
  // 로 "N개 솔루션 참조" 를 surface → 사용자가 어떤 축적 지식이 붙었는지 인식.
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Claude InstallPlan — feat/codex-support Phase 1 (P1-2)
3
3
  *
4
- * `npm install` postinstall.js 의 *Claude 측* 4 작업을 module 로 분리.
4
+ * `npm install` postinstall.js 의 *Claude 측* 5 작업을 module 로 분리.
5
5
  * `forgen install claude` CLI 가 호출 + (P1-6 에서) postinstall.js 도 위임.
6
6
  *
7
- * 4 작업:
7
+ * 5 작업:
8
8
  * 1. Plugin cache: ~/.claude/plugins/cache/forgen-local/forgen/<ver>/ 작성 + installed_plugins.json 등록
9
9
  * 2. Slash commands: ~/.claude/commands/forgen/*.md 생성 (forgen-managed marker)
10
10
  * 3. Settings hooks injection: ~/.claude/settings.json 의 hooks 머지 (forgen entry idempotent)
11
11
  * 4. MCP register: ~/.claude.json 에 mcpServers.forgen-compound 추가
12
+ * 5. Dev-guide skills: ~/.claude/skills/forgen-<stack>-<skill>/ 설치 (forgen-managed only)
12
13
  *
13
14
  * 사용자 비-forgen 자산 보존 + 재실행 idempotent.
14
15
  */
@@ -31,5 +32,8 @@ export interface ClaudeInstallResult {
31
32
  hooksInjected: number;
32
33
  mcpRegistered: boolean;
33
34
  mcpAlreadyPresent: boolean;
35
+ skillsPath: string;
36
+ skillsInstalled: number;
37
+ skillsRemoved: number;
34
38
  }
35
39
  export declare function planClaudeInstall(opts: ClaudeInstallOptions): ClaudeInstallResult;
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Claude InstallPlan — feat/codex-support Phase 1 (P1-2)
3
3
  *
4
- * `npm install` postinstall.js 의 *Claude 측* 4 작업을 module 로 분리.
4
+ * `npm install` postinstall.js 의 *Claude 측* 5 작업을 module 로 분리.
5
5
  * `forgen install claude` CLI 가 호출 + (P1-6 에서) postinstall.js 도 위임.
6
6
  *
7
- * 4 작업:
7
+ * 5 작업:
8
8
  * 1. Plugin cache: ~/.claude/plugins/cache/forgen-local/forgen/<ver>/ 작성 + installed_plugins.json 등록
9
9
  * 2. Slash commands: ~/.claude/commands/forgen/*.md 생성 (forgen-managed marker)
10
10
  * 3. Settings hooks injection: ~/.claude/settings.json 의 hooks 머지 (forgen entry idempotent)
11
11
  * 4. MCP register: ~/.claude.json 에 mcpServers.forgen-compound 추가
12
+ * 5. Dev-guide skills: ~/.claude/skills/forgen-<stack>-<skill>/ 설치 (forgen-managed only)
12
13
  *
13
14
  * 사용자 비-forgen 자산 보존 + 재실행 idempotent.
14
15
  */
@@ -234,6 +235,72 @@ function registerMcpInClaudeJson(opts) {
234
235
  fs.writeFileSync(claudeJsonPath, `${JSON.stringify(claudeJson, null, 2)}\n`);
235
236
  return { registered: !alreadyPresent, alreadyPresent };
236
237
  }
238
+ // ── 5. Dev-guide skills ────────────────────────────────────────────────
239
+ const FORGEN_SKILL_PREFIX = 'forgen-';
240
+ function installDevGuideSkills(opts) {
241
+ const { pkgRoot, skillsDir, dryRun } = opts;
242
+ const devGuideRoot = path.join(pkgRoot, 'assets', 'dev-guide');
243
+ // Collect all SKILL.md entries: assets/dev-guide/{tier}/skills/{stack}/{skill}/SKILL.md
244
+ const entries = [];
245
+ if (fs.existsSync(devGuideRoot)) {
246
+ for (const tier of fs.readdirSync(devGuideRoot)) {
247
+ const skillsBase = path.join(devGuideRoot, tier, 'skills');
248
+ if (!fs.existsSync(skillsBase))
249
+ continue;
250
+ for (const stack of fs.readdirSync(skillsBase)) {
251
+ const stackDir = path.join(skillsBase, stack);
252
+ if (!fs.statSync(stackDir).isDirectory())
253
+ continue;
254
+ for (const skill of fs.readdirSync(stackDir)) {
255
+ const skillMd = path.join(stackDir, skill, 'SKILL.md');
256
+ if (fs.existsSync(skillMd)) {
257
+ entries.push({ name: `${FORGEN_SKILL_PREFIX}${stack}-${skill}`, src: skillMd });
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ if (dryRun) {
264
+ return { skillsPath: skillsDir, skillsInstalled: entries.length, skillsRemoved: 0 };
265
+ }
266
+ fs.mkdirSync(skillsDir, { recursive: true });
267
+ // Remove stale forgen-* skill dirs (idempotent re-install, do not touch user's own skills)
268
+ let removed = 0;
269
+ for (const entry of fs.readdirSync(skillsDir)) {
270
+ if (!entry.startsWith(FORGEN_SKILL_PREFIX))
271
+ continue;
272
+ const fullPath = path.join(skillsDir, entry);
273
+ if (fs.statSync(fullPath).isDirectory()) {
274
+ fs.rmSync(fullPath, { recursive: true, force: true });
275
+ removed += 1;
276
+ }
277
+ }
278
+ // Install each skill via symlink → cpSync fallback (mirrors plugin cache pattern)
279
+ let installed = 0;
280
+ for (const { name, src } of entries) {
281
+ const targetDir = path.join(skillsDir, name);
282
+ fs.mkdirSync(targetDir, { recursive: true });
283
+ const targetFile = path.join(targetDir, 'SKILL.md');
284
+ let linked = false;
285
+ let symlinkErr = null;
286
+ try {
287
+ fs.symlinkSync(src, targetFile, 'file');
288
+ linked = true;
289
+ }
290
+ catch (e) {
291
+ symlinkErr = e;
292
+ }
293
+ if (!linked && symlinkErr) {
294
+ const code = symlinkErr.code ?? 'UNKNOWN';
295
+ process.stderr.write(`[forgen] symlink ${src} → ${targetFile} failed (${code}); falling back to copyFile.\n`);
296
+ }
297
+ if (!linked) {
298
+ fs.copyFileSync(src, targetFile);
299
+ }
300
+ installed += 1;
301
+ }
302
+ return { skillsPath: skillsDir, skillsInstalled: installed, skillsRemoved: removed };
303
+ }
237
304
  // ── public ─────────────────────────────────────────────────────────────
238
305
  export function planClaudeInstall(opts) {
239
306
  if (!opts.pkgRoot || !fs.existsSync(opts.pkgRoot)) {
@@ -249,12 +316,14 @@ export function planClaudeInstall(opts) {
249
316
  const slashCommandsDir = path.join(claudeDir, 'commands', 'forgen');
250
317
  const settingsPath = path.join(claudeDir, 'settings.json');
251
318
  const claudeJsonPath = path.join(homeDir, '.claude.json');
319
+ const skillsDir = path.join(claudeDir, 'skills');
252
320
  const pluginCacheWritten = writePluginCache({ pkgRoot: opts.pkgRoot, cacheDir, pluginsDir, version, dryRun });
253
321
  const slashCommandsCount = writeSlashCommands({ pkgRoot: opts.pkgRoot, targetDir: slashCommandsDir, dryRun });
254
322
  const hooksInjected = injectHooksIntoSettings({ pkgRoot: opts.pkgRoot, settingsPath, dryRun });
255
323
  const mcp = registerMcp
256
324
  ? registerMcpInClaudeJson({ pkgRoot: opts.pkgRoot, claudeJsonPath, dryRun })
257
325
  : { registered: false, alreadyPresent: false };
326
+ const skills = installDevGuideSkills({ pkgRoot: opts.pkgRoot, skillsDir, dryRun });
258
327
  return {
259
328
  homeDir,
260
329
  pluginCachePath: cacheDir,
@@ -265,5 +334,8 @@ export function planClaudeInstall(opts) {
265
334
  hooksInjected,
266
335
  mcpRegistered: mcp.registered,
267
336
  mcpAlreadyPresent: mcp.alreadyPresent,
337
+ skillsPath: skills.skillsPath,
338
+ skillsInstalled: skills.skillsInstalled,
339
+ skillsRemoved: skills.skillsRemoved,
268
340
  };
269
341
  }
@@ -40,5 +40,9 @@ export interface CodexInstallResult {
40
40
  /** P3-3: AGENTS.md (cwd) 에 forgen rule block 인젝션 여부 */
41
41
  agentsMdPath: string;
42
42
  agentsMdInjected: boolean;
43
+ /** v0.4.9: dev-guide skills (~/.codex/skills) 설치 결과 */
44
+ devGuideSkillsPath: string;
45
+ devGuideSkillsInstalled: number;
46
+ devGuideSkillsRemoved: number;
43
47
  }
44
48
  export declare function planCodexInstall(opts: CodexInstallOptions): CodexInstallResult;
@@ -150,6 +150,12 @@ export function planCodexInstall(opts) {
150
150
  // pkgRoot 의 git repo root 의 AGENTS.md, 또는 explicit override.
151
151
  const agentsMdPath = opts.agentsMdPath ?? resolveAgentsMdPath(opts.pkgRoot);
152
152
  const agentsResult = upsertForgenRulesInAgentsMd({ agentsMdPath, pkgRoot: opts.pkgRoot, dryRun: opts.dryRun ?? false });
153
+ // 8) v0.4.9: dev-guide skills → ~/.codex/skills/forgen-<stack>-<skill>/SKILL.md
154
+ const devGuideResult = installDevGuideSkillsToCodex({
155
+ pkgRoot: opts.pkgRoot,
156
+ codexHome,
157
+ dryRun: opts.dryRun ?? false,
158
+ });
153
159
  return {
154
160
  codexHome,
155
161
  hooksPath,
@@ -163,8 +169,73 @@ export function planCodexInstall(opts) {
163
169
  skillsPath,
164
170
  agentsMdPath,
165
171
  agentsMdInjected: agentsResult.injected,
172
+ devGuideSkillsPath: devGuideResult.devGuideSkillsPath,
173
+ devGuideSkillsInstalled: devGuideResult.devGuideSkillsInstalled,
174
+ devGuideSkillsRemoved: devGuideResult.devGuideSkillsRemoved,
166
175
  };
167
176
  }
177
+ // ── v0.4.9: dev-guide skills → ~/.codex/skills ────────────────────────
178
+ // dev-guide prefix pattern: forgen-<stack>-<skill> (e.g. forgen-react-fe-build)
179
+ // 반드시 stack 이 react|vue|node|go 인 것만 매칭 — forgen 자체 commands 보존
180
+ const DEV_GUIDE_SKILL_PATTERN = /^forgen-(react|vue|node|go)-/;
181
+ function installDevGuideSkillsToCodex(opts) {
182
+ const devGuideRoot = path.join(opts.pkgRoot, 'assets', 'dev-guide');
183
+ const codexSkillsDir = path.join(opts.codexHome, 'skills');
184
+ if (!fs.existsSync(devGuideRoot)) {
185
+ return { devGuideSkillsPath: codexSkillsDir, devGuideSkillsInstalled: 0, devGuideSkillsRemoved: 0 };
186
+ }
187
+ // Collect entries: assets/dev-guide/{tier}/skills/{stack}/{skill}/SKILL.md
188
+ const entries = [];
189
+ for (const tier of fs.readdirSync(devGuideRoot)) {
190
+ const skillsBase = path.join(devGuideRoot, tier, 'skills');
191
+ if (!fs.existsSync(skillsBase))
192
+ continue;
193
+ for (const stack of fs.readdirSync(skillsBase)) {
194
+ const stackDir = path.join(skillsBase, stack);
195
+ if (!fs.statSync(stackDir).isDirectory())
196
+ continue;
197
+ for (const skill of fs.readdirSync(stackDir)) {
198
+ const skillMd = path.join(stackDir, skill, 'SKILL.md');
199
+ if (fs.existsSync(skillMd)) {
200
+ entries.push({ name: `forgen-${stack}-${skill}`, src: skillMd });
201
+ }
202
+ }
203
+ }
204
+ }
205
+ if (opts.dryRun) {
206
+ return { devGuideSkillsPath: codexSkillsDir, devGuideSkillsInstalled: entries.length, devGuideSkillsRemoved: 0 };
207
+ }
208
+ fs.mkdirSync(codexSkillsDir, { recursive: true });
209
+ // Stale cleanup: dev-guide pattern 만 정리 (forgen 자체 commands 보존)
210
+ let removed = 0;
211
+ for (const entry of fs.readdirSync(codexSkillsDir)) {
212
+ if (DEV_GUIDE_SKILL_PATTERN.test(entry)) {
213
+ try {
214
+ fs.rmSync(path.join(codexSkillsDir, entry), { recursive: true, force: true });
215
+ removed++;
216
+ }
217
+ catch { /* best-effort */ }
218
+ }
219
+ }
220
+ // Install via symlink → copyFileSync fallback
221
+ let installed = 0;
222
+ for (const { name, src } of entries) {
223
+ const dstDir = path.join(codexSkillsDir, name);
224
+ fs.mkdirSync(dstDir, { recursive: true });
225
+ const dst = path.join(dstDir, 'SKILL.md');
226
+ let linked = false;
227
+ try {
228
+ fs.symlinkSync(src, dst, 'file');
229
+ linked = true;
230
+ }
231
+ catch { /* fallback */ }
232
+ if (!linked) {
233
+ fs.copyFileSync(src, dst);
234
+ }
235
+ installed++;
236
+ }
237
+ return { devGuideSkillsPath: codexSkillsDir, devGuideSkillsInstalled: installed, devGuideSkillsRemoved: removed };
238
+ }
168
239
  // ── P3-3: Codex skills install ────────────────────────────────────────
169
240
  function installCodexSkills(opts) {
170
241
  const { sourceDir, targetDir, dryRun } = opts;
@@ -109,6 +109,7 @@ export function renderResult(result, dryRun) {
109
109
  lines.push(` slash commands: ${result.claude.slashCommandsCount} → ${result.claude.slashCommandsPath}`);
110
110
  lines.push(` settings.json hooks: ${result.claude.hooksInjected}`);
111
111
  lines.push(` MCP: ${result.claude.mcpAlreadyPresent ? 'already present' : (result.claude.mcpRegistered ? 'registered' : 'skipped')}`);
112
+ lines.push(` skills: ${result.claude.skillsInstalled ?? 0} installed → ${result.claude.skillsPath ?? ''}`);
112
113
  }
113
114
  if (result.codex) {
114
115
  lines.push('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooojin/forgen",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "preferGlobal": true,
5
5
  "main": "dist/lib.js",
6
6
  "types": "./dist/lib.d.ts",
@@ -47,19 +47,19 @@
47
47
  ],
48
48
  "repository": {
49
49
  "type": "git",
50
- "url": "https://github.com/forgen-team/forgen.git"
50
+ "url": "git+https://github.com/forgen-team/forgen.git"
51
51
  },
52
52
  "engines": {
53
- "node": ">=20.0.0"
53
+ "node": ">=22.0.0"
54
54
  },
55
55
  "type": "module",
56
56
  "workspaces": [
57
57
  "packages/*"
58
58
  ],
59
59
  "bin": {
60
- "forgen": "./dist/cli.js",
61
- "fgx": "./dist/fgx.js",
62
- "forgen-mcp": "./dist/mcp/server.js"
60
+ "forgen": "dist/cli.js",
61
+ "fgx": "dist/fgx.js",
62
+ "forgen-mcp": "dist/mcp/server.js"
63
63
  },
64
64
  "files": [
65
65
  "dist/",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.8",
4
+ "version": "0.4.9",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",
@@ -746,6 +746,125 @@ function cleanLegacyMcpFromSettings(settings) {
746
746
  }
747
747
  }
748
748
 
749
+ /**
750
+ * dev-guide 스킬 14개 자동 설치 — assets/dev-guide/{fe,be}/skills/{stack}/{skill}/SKILL.md
751
+ * → ~/.claude/skills/forgen-<stack>-<skill>/SKILL.md
752
+ *
753
+ * forgen- prefix 로 사용자 own skills 와 격리. idempotent 재실행 시 forgen-* 만 정리 후 재설치.
754
+ */
755
+ function installDevGuideSkills(home) {
756
+ const devGuideRoot = join(PKG_ROOT, 'assets', 'dev-guide');
757
+ if (!existsSync(devGuideRoot)) {
758
+ console.log('[forgen] dev-guide assets not found — skipping skill install');
759
+ return 0;
760
+ }
761
+
762
+ const userSkillsDir = join(home, '.claude', 'skills');
763
+ mkdirSync(userSkillsDir, { recursive: true });
764
+
765
+ // 1. 기존 forgen-* 디렉토리 정리 (사용자 own 보존)
766
+ let removed = 0;
767
+ for (const entry of readdirSync(userSkillsDir)) {
768
+ if (entry.startsWith('forgen-')) {
769
+ try { rmSync(join(userSkillsDir, entry), { recursive: true, force: true }); removed++; } catch { /* best-effort */ }
770
+ }
771
+ }
772
+
773
+ // 2. assets/dev-guide/{side}/skills/{stack}/{skill}/SKILL.md 수집
774
+ let installed = 0;
775
+ for (const side of ['fe', 'be']) {
776
+ const sideSkillsDir = join(devGuideRoot, side, 'skills');
777
+ if (!existsSync(sideSkillsDir)) continue;
778
+ for (const stack of readdirSync(sideSkillsDir)) {
779
+ const stackDir = join(sideSkillsDir, stack);
780
+ if (!statSync(stackDir).isDirectory()) continue;
781
+ for (const skill of readdirSync(stackDir)) {
782
+ const skillSrc = join(stackDir, skill);
783
+ const skillFile = join(skillSrc, 'SKILL.md');
784
+ if (!existsSync(skillFile)) continue;
785
+
786
+ // 대상: ~/.claude/skills/forgen-<stack>-<skill>/SKILL.md
787
+ const dstDir = join(userSkillsDir, `forgen-${stack}-${skill}`);
788
+ mkdirSync(dstDir, { recursive: true });
789
+ const dst = join(dstDir, 'SKILL.md');
790
+
791
+ // symlink → fallback cpSync
792
+ try {
793
+ if (existsSync(dst)) rmSync(dst);
794
+ symlinkSync(skillFile, dst, 'file');
795
+ } catch {
796
+ cpSync(skillFile, dst);
797
+ }
798
+ installed++;
799
+ }
800
+ }
801
+ }
802
+
803
+ // 3. sudo 케이스 ownership 회복
804
+ fixOwnership(userSkillsDir);
805
+
806
+ console.log(`[forgen] dev-guide skills: ${installed} installed${removed > 0 ? ` (${removed} stale removed)` : ''}`);
807
+ return installed;
808
+ }
809
+
810
+ /**
811
+ * v0.4.9: dev-guide 14 skills → ~/.codex/skills/forgen-<stack>-<skill>/SKILL.md
812
+ * forgen 자체 commands(forgen-compound 등) 와 prefix 겹치지 않도록
813
+ * DEV_GUIDE_SKILL_PATTERN(/^forgen-(react|vue|node|go)-/) 으로 stale 정리.
814
+ */
815
+ const DEV_GUIDE_SKILL_PATTERN = /^forgen-(react|vue|node|go)-/;
816
+
817
+ function installDevGuideSkillsToCodex(home) {
818
+ const devGuideRoot = join(PKG_ROOT, 'assets', 'dev-guide');
819
+ if (!existsSync(devGuideRoot)) {
820
+ console.log('[forgen] dev-guide assets not found — codex skill install skipped');
821
+ return 0;
822
+ }
823
+
824
+ const codexSkillsDir = join(home, '.codex', 'skills');
825
+ mkdirSync(codexSkillsDir, { recursive: true });
826
+
827
+ // 1. stale forgen-<stack>-<skill> 정리 (forgen 자체 commands 보존)
828
+ let removed = 0;
829
+ for (const entry of readdirSync(codexSkillsDir)) {
830
+ if (DEV_GUIDE_SKILL_PATTERN.test(entry)) {
831
+ try { rmSync(join(codexSkillsDir, entry), { recursive: true, force: true }); removed++; } catch { /* best-effort */ }
832
+ }
833
+ }
834
+
835
+ // 2. assets/dev-guide/{tier}/skills/{stack}/{skill}/SKILL.md 수집
836
+ let installed = 0;
837
+ for (const tier of readdirSync(devGuideRoot)) {
838
+ const sideSkillsDir = join(devGuideRoot, tier, 'skills');
839
+ if (!existsSync(sideSkillsDir)) continue;
840
+ for (const stack of readdirSync(sideSkillsDir)) {
841
+ const stackDir = join(sideSkillsDir, stack);
842
+ if (!statSync(stackDir).isDirectory()) continue;
843
+ for (const skill of readdirSync(stackDir)) {
844
+ const skillFile = join(stackDir, skill, 'SKILL.md');
845
+ if (!existsSync(skillFile)) continue;
846
+
847
+ const dstDir = join(codexSkillsDir, `forgen-${stack}-${skill}`);
848
+ mkdirSync(dstDir, { recursive: true });
849
+ const dst = join(dstDir, 'SKILL.md');
850
+
851
+ // symlink → fallback cpSync
852
+ try {
853
+ if (existsSync(dst)) rmSync(dst);
854
+ symlinkSync(skillFile, dst, 'file');
855
+ } catch {
856
+ cpSync(skillFile, dst);
857
+ }
858
+ installed++;
859
+ }
860
+ }
861
+ }
862
+
863
+ fixOwnership(codexSkillsDir);
864
+ console.log(`[forgen] codex dev-guide skills: ${installed} installed${removed > 0 ? ` (${removed} stale removed)` : ''}`);
865
+ return installed;
866
+ }
867
+
749
868
  // ── Main ──
750
869
 
751
870
  /**
@@ -927,6 +1046,21 @@ async function main() {
927
1046
  console.error(`[forgen] starter pack failed: ${err?.message ?? err}`);
928
1047
  }
929
1048
 
1049
+ // ── 8b. dev-guide 스킬 자동 설치 (claude) ──
1050
+ try {
1051
+ installDevGuideSkills(HOME);
1052
+ } catch (e) {
1053
+ // postinstall 의 "fail-open" 원칙 — npm install 깨뜨리지 않음
1054
+ console.log(`[forgen] dev-guide skills 설치 스킵 (${e?.message ?? e})`);
1055
+ }
1056
+
1057
+ // ── 8c. dev-guide 스킬 자동 설치 (codex) ──
1058
+ try {
1059
+ installDevGuideSkillsToCodex(HOME);
1060
+ } catch (e) {
1061
+ console.log(`[forgen] codex dev-guide skills 설치 스킵 (${e?.message ?? e})`);
1062
+ }
1063
+
930
1064
  // sudo 실행 시 파일 소유권을 실제 유저로 변경
931
1065
  fixOwnership(join(HOME, '.claude'), join(HOME, '.forgen'));
932
1066