@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
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Forgen — Dashboard CLI (P3)
3
+ *
4
+ * `fgx status [--watch] [--json] [--interval N]`
5
+ * ANSI box-drawing 기반 상태 대시보드. TUI 라이브러리 없음.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { getUsageStats } from './usage-telemetry.js';
10
+ import { classifySolutions } from './lifecycle-classifier.js';
11
+ import { STATE_DIR } from './paths.js';
12
+ import { FORGEN_HOME } from './paths.js';
13
+ // ── ANSI ──────────────────────────────────────────────────────────────────────
14
+ const isTTY = process.stdout.isTTY;
15
+ const C = {
16
+ reset: isTTY ? '\x1b[0m' : '',
17
+ bold: isTTY ? '\x1b[1m' : '',
18
+ dim: isTTY ? '\x1b[2m' : '',
19
+ cyan: isTTY ? '\x1b[36m' : '',
20
+ yellow: isTTY ? '\x1b[33m' : '',
21
+ green: isTTY ? '\x1b[32m' : '',
22
+ red: isTTY ? '\x1b[31m' : '',
23
+ };
24
+ const BOX_WIDTH = 66;
25
+ function bar(pct, width = 10) {
26
+ const filled = Math.round(pct * width);
27
+ const empty = width - filled;
28
+ return '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty));
29
+ }
30
+ // 런타임에 정규식 구성 — biome 의 noControlCharactersInRegex 우회.
31
+ // ESC (0x1B) 는 ANSI escape 의 시작이지만 literal regex 에는 못 박힘.
32
+ const ANSI_STRIP_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
33
+ function boxLine(content) {
34
+ const plain = content.replace(ANSI_STRIP_RE, '');
35
+ const padding = BOX_WIDTH - 2 - plain.length;
36
+ return `│ ${content}${' '.repeat(Math.max(0, padding))} │`;
37
+ }
38
+ function boxEmpty() {
39
+ return `│${' '.repeat(BOX_WIDTH - 2)}│`;
40
+ }
41
+ function boxTop() {
42
+ return `┌─ ${C.bold}${C.cyan}forgen status${C.reset}` +
43
+ ' ' + '─'.repeat(BOX_WIDTH - 17) + '┐';
44
+ }
45
+ function boxBottom() {
46
+ return `└${'─'.repeat(BOX_WIDTH - 2)}┘`;
47
+ }
48
+ function collectData() {
49
+ const now = new Date();
50
+ // usage
51
+ const usage = (() => {
52
+ try {
53
+ return getUsageStats();
54
+ }
55
+ catch {
56
+ return { hour5: { claude: 0, codex: 0, total: 0 }, week: { claude: 0, codex: 0, total: 0 } };
57
+ }
58
+ })();
59
+ // today extracted: me/solutions/*.md mtime이 오늘인 것 (또는 last-extraction.json)
60
+ const todayExtracted = (() => {
61
+ try {
62
+ const lastExtPath = path.join(STATE_DIR, 'last-extraction.json');
63
+ if (fs.existsSync(lastExtPath)) {
64
+ const data = JSON.parse(fs.readFileSync(lastExtPath, 'utf-8'));
65
+ const ts = data.ts ?? 0;
66
+ const isToday = new Date(ts).toDateString() === now.toDateString();
67
+ if (isToday && typeof data.count === 'number')
68
+ return data.count;
69
+ }
70
+ }
71
+ catch { /* ignore */ }
72
+ // fallback: count solutions modified today
73
+ try {
74
+ const solutionsDir = path.join(FORGEN_HOME, 'me', 'solutions');
75
+ if (!fs.existsSync(solutionsDir))
76
+ return 0;
77
+ const files = fs.readdirSync(solutionsDir).filter(f => f.endsWith('.md'));
78
+ const todayStr = now.toDateString();
79
+ return files.filter(f => {
80
+ try {
81
+ const stat = fs.statSync(path.join(solutionsDir, f));
82
+ return new Date(stat.mtimeMs).toDateString() === todayStr;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }).length;
88
+ }
89
+ catch {
90
+ return 0;
91
+ }
92
+ })();
93
+ // lifecycle
94
+ const classified = (() => {
95
+ try {
96
+ return classifySolutions();
97
+ }
98
+ catch {
99
+ return [];
100
+ }
101
+ })();
102
+ const counts = { hot: 0, warm: 0, cold: 0, dead: 0, new: 0 };
103
+ for (const c of classified)
104
+ counts[c.lifecycle]++;
105
+ const topHot = classified
106
+ .filter(c => c.lifecycle === 'hot')
107
+ .sort((a, b) => b.acted_90d - a.acted_90d)
108
+ .slice(0, 5)
109
+ .map(c => ({
110
+ id: c.solutionId,
111
+ surfaced: c.surfaced_90d,
112
+ acted: c.acted_90d,
113
+ rate: c.hitRate ?? 0,
114
+ }));
115
+ // rate-limit misses (7d)
116
+ const rateLimitMisses7d = (() => {
117
+ try {
118
+ const missPath = path.join(STATE_DIR, 'rate-limit-misses.jsonl');
119
+ if (!fs.existsSync(missPath))
120
+ return 0;
121
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
122
+ const lines = fs.readFileSync(missPath, 'utf-8').split('\n').filter(Boolean);
123
+ return lines.filter(l => {
124
+ try {
125
+ const obj = JSON.parse(l);
126
+ return typeof obj.ts === 'number' && obj.ts >= cutoff;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }).length;
132
+ }
133
+ catch {
134
+ return 0;
135
+ }
136
+ })();
137
+ return {
138
+ timestamp: now.toISOString(),
139
+ usage,
140
+ todayExtracted,
141
+ solutions: {
142
+ total: classified.length,
143
+ ...counts,
144
+ topHot,
145
+ classified,
146
+ },
147
+ rateLimitMisses7d,
148
+ };
149
+ }
150
+ // ── Render ─────────────────────────────────────────────────────────────────────
151
+ function renderTTY(data) {
152
+ const lines = [];
153
+ lines.push(boxTop());
154
+ lines.push(boxEmpty());
155
+ // Usage
156
+ lines.push(boxLine(`${C.bold}Usage${C.reset}`));
157
+ const h5total = data.usage.hour5.total;
158
+ const h5pct = Math.min(1, h5total / Math.max(h5total + 10, 50));
159
+ const h5bar = bar(h5pct);
160
+ lines.push(boxLine(` 5h window: ${C.yellow}${h5bar}${C.reset} (${h5total} tool calls)`));
161
+ const wktotal = data.usage.week.total;
162
+ const wkpct = Math.min(1, wktotal / Math.max(wktotal + 10, 100));
163
+ const wkbar = bar(wkpct);
164
+ lines.push(boxLine(` weekly: ${C.yellow}${wkbar}${C.reset} (${wktotal} tool calls)`));
165
+ lines.push(boxEmpty());
166
+ // Today compound
167
+ lines.push(boxLine(`${C.bold}Today's compound${C.reset}`));
168
+ lines.push(boxLine(` extracted: ${data.todayExtracted} solutions`));
169
+ lines.push(boxEmpty());
170
+ // Solutions
171
+ lines.push(boxLine(`${C.bold}Solutions${C.reset} (${data.solutions.total} total)`));
172
+ const lifecycleLine = ` ${C.red}🔥 hot:${C.reset} ${data.solutions.hot}` +
173
+ ` ${C.yellow}🟡 warm:${C.reset} ${data.solutions.warm}` +
174
+ ` ${C.cyan}🥶 cold:${C.reset} ${data.solutions.cold}` +
175
+ ` ${C.dim}💀 dead:${C.reset} ${data.solutions.dead}` +
176
+ ` ${C.green}🌱 new:${C.reset} ${data.solutions.new}`;
177
+ lines.push(boxLine(lifecycleLine));
178
+ lines.push(boxEmpty());
179
+ // Top 5 hot
180
+ if (data.solutions.topHot.length > 0) {
181
+ lines.push(boxLine(`${C.bold}Top ${data.solutions.topHot.length} hot (90d)${C.reset}`));
182
+ for (const h of data.solutions.topHot) {
183
+ const pctStr = `${Math.round(h.rate * 100)}%`;
184
+ const idShort = h.id.length > 36 ? h.id.slice(0, 33) + '...' : h.id;
185
+ lines.push(boxLine(` · ${C.yellow}${idShort}${C.reset} surf=${h.surfaced} acted=${h.acted} (${pctStr})`));
186
+ }
187
+ lines.push(boxEmpty());
188
+ }
189
+ // Rate-limit
190
+ lines.push(boxLine(`${C.bold}Rate-limit${C.reset}`));
191
+ lines.push(boxLine(` misses (7d): ${data.rateLimitMisses7d}`));
192
+ lines.push(boxEmpty());
193
+ // Last update
194
+ lines.push(boxLine(`${C.dim}Last update: ${data.timestamp}${C.reset}`));
195
+ lines.push(boxBottom());
196
+ return lines.join('\n');
197
+ }
198
+ // ── Public API ────────────────────────────────────────────────────────────────
199
+ export async function runDashboard(opts) {
200
+ const intervalSec = opts.intervalSec ?? 5;
201
+ const render = () => {
202
+ const data = collectData();
203
+ if (opts.json) {
204
+ // Strip classified array (verbose) from JSON output for cleaner schema
205
+ const { solutions: { classified: _c, ...solutionStats }, ...rest } = data;
206
+ process.stdout.write(JSON.stringify({ ...rest, solutions: solutionStats }, null, 2) + '\n');
207
+ return;
208
+ }
209
+ const output = renderTTY(data);
210
+ if (opts.watch) {
211
+ process.stdout.write('\x1b[2J\x1b[H'); // clear screen
212
+ }
213
+ process.stdout.write(output + '\n');
214
+ };
215
+ render();
216
+ if (opts.watch) {
217
+ const interval = setInterval(render, intervalSec * 1000);
218
+ process.on('SIGINT', () => {
219
+ clearInterval(interval);
220
+ process.stdout.write('\n');
221
+ process.exit(0);
222
+ });
223
+ // Keep process alive
224
+ await new Promise(() => { });
225
+ }
226
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * dev-guide-injector — fgx init 시 dev-guide principles 를
3
+ * Claude (.claude/rules/dev-guide-principles.md) + Codex (AGENTS.md managed block)
4
+ * 양쪽에 stack-aware 자동 inject.
5
+ */
6
+ export interface DetectedStack {
7
+ side: 'fe' | 'be';
8
+ stack: 'react' | 'vue' | 'node' | 'go';
9
+ principlesFiles: string[];
10
+ }
11
+ export interface InjectResult {
12
+ stack: DetectedStack | null;
13
+ claudeRulePath: string;
14
+ claudeRuleWritten: boolean;
15
+ agentsMdPath: string;
16
+ agentsMdInjected: boolean;
17
+ bytesWritten: number;
18
+ }
19
+ export interface InjectOptions {
20
+ cwd: string;
21
+ pkgRoot: string;
22
+ dryRun?: boolean;
23
+ }
24
+ export declare function detectStack(cwd: string): DetectedStack | null;
25
+ export declare function readPrinciples(pkgRoot: string, side: 'fe' | 'be', stack: string): string;
26
+ export declare function injectDevGuidePrinciples(opts: InjectOptions): InjectResult;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * dev-guide-injector — fgx init 시 dev-guide principles 를
3
+ * Claude (.claude/rules/dev-guide-principles.md) + Codex (AGENTS.md managed block)
4
+ * 양쪽에 stack-aware 자동 inject.
5
+ */
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ // ── 상수 ──────────────────────────────────────────────────────────────────────
9
+ const AGENTS_MD_BEGIN = '<!-- >>> forgen-managed-rules -->';
10
+ const AGENTS_MD_END = '<!-- <<< forgen-managed-rules -->';
11
+ // ── detectStack ───────────────────────────────────────────────────────────────
12
+ export function detectStack(cwd) {
13
+ const pkgPath = path.join(cwd, 'package.json');
14
+ if (fs.existsSync(pkgPath)) {
15
+ try {
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
17
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
18
+ if ('react' in deps || 'next' in deps) {
19
+ return { side: 'fe', stack: 'react', principlesFiles: ['common.md', 'react.md'] };
20
+ }
21
+ if ('vue' in deps || 'nuxt' in deps) {
22
+ return { side: 'fe', stack: 'vue', principlesFiles: ['common.md', 'vue.md'] };
23
+ }
24
+ return { side: 'be', stack: 'node', principlesFiles: ['common.md', 'node.md'] };
25
+ }
26
+ catch {
27
+ // JSON parse 실패 — fall through to go.mod check
28
+ }
29
+ }
30
+ if (fs.existsSync(path.join(cwd, 'go.mod'))) {
31
+ return { side: 'be', stack: 'go', principlesFiles: ['common.md', 'go.md'] };
32
+ }
33
+ return null;
34
+ }
35
+ // ── readPrinciples ────────────────────────────────────────────────────────────
36
+ export function readPrinciples(pkgRoot, side, stack) {
37
+ const principlesDir = path.join(pkgRoot, 'assets', 'dev-guide', side, 'principles');
38
+ const commonContent = fs.readFileSync(path.join(principlesDir, 'common.md'), 'utf-8');
39
+ const stackContent = fs.readFileSync(path.join(principlesDir, `${stack}.md`), 'utf-8');
40
+ return [
41
+ `<!-- forgen dev-guide principles (auto-generated, do not edit) -->`,
42
+ `<!-- source: assets/dev-guide/${side}/principles/common.md + ${stack}.md -->`,
43
+ ``,
44
+ `# common (${side})`,
45
+ commonContent.trimEnd(),
46
+ ``,
47
+ `---`,
48
+ ``,
49
+ `# ${stack}`,
50
+ stackContent.trimEnd(),
51
+ ``,
52
+ ].join('\n');
53
+ }
54
+ // ── injectDevGuidePrinciples ──────────────────────────────────────────────────
55
+ export function injectDevGuidePrinciples(opts) {
56
+ const { cwd, pkgRoot, dryRun = false } = opts;
57
+ const claudeRulePath = path.join(cwd, '.claude', 'rules', 'dev-guide-principles.md');
58
+ const agentsMdPath = path.join(cwd, 'AGENTS.md');
59
+ const emptyResult = {
60
+ stack: null,
61
+ claudeRulePath,
62
+ claudeRuleWritten: false,
63
+ agentsMdPath,
64
+ agentsMdInjected: false,
65
+ bytesWritten: 0,
66
+ };
67
+ const detected = detectStack(cwd);
68
+ if (!detected)
69
+ return emptyResult;
70
+ const body = readPrinciples(pkgRoot, detected.side, detected.stack);
71
+ // ── Claude side ──
72
+ const claudeRuleWritten = writeClaudeRule({ claudeRulePath, body, dryRun });
73
+ // ── Codex side ──
74
+ const agentsMdInjected = upsertAgentsMd({ agentsMdPath, body, dryRun });
75
+ return {
76
+ stack: detected,
77
+ claudeRulePath,
78
+ claudeRuleWritten,
79
+ agentsMdPath,
80
+ agentsMdInjected,
81
+ bytesWritten: dryRun ? 0 : body.length,
82
+ };
83
+ }
84
+ // ── helpers ───────────────────────────────────────────────────────────────────
85
+ function writeClaudeRule(opts) {
86
+ const { claudeRulePath, body, dryRun } = opts;
87
+ const existing = fs.existsSync(claudeRulePath)
88
+ ? fs.readFileSync(claudeRulePath, 'utf-8')
89
+ : null;
90
+ if (existing === body)
91
+ return false;
92
+ if (!dryRun) {
93
+ fs.mkdirSync(path.dirname(claudeRulePath), { recursive: true });
94
+ fs.writeFileSync(claudeRulePath, body, 'utf-8');
95
+ }
96
+ return true;
97
+ }
98
+ function escapeRegex(s) {
99
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
100
+ }
101
+ function buildDevGuideBlock(body) {
102
+ return [AGENTS_MD_BEGIN, body.trimEnd(), AGENTS_MD_END].join('\n');
103
+ }
104
+ function upsertAgentsMd(opts) {
105
+ const { agentsMdPath, body, dryRun } = opts;
106
+ const block = buildDevGuideBlock(body);
107
+ const current = fs.existsSync(agentsMdPath) ? fs.readFileSync(agentsMdPath, 'utf-8') : '';
108
+ const reMarker = new RegExp(`${escapeRegex(AGENTS_MD_BEGIN)}[\\s\\S]*?${escapeRegex(AGENTS_MD_END)}`);
109
+ const hasBlock = reMarker.test(current);
110
+ let newContent;
111
+ if (hasBlock) {
112
+ newContent = current.replace(reMarker, block);
113
+ }
114
+ else {
115
+ const beginIdx = current.indexOf(AGENTS_MD_BEGIN);
116
+ const endIdx = current.indexOf(AGENTS_MD_END);
117
+ if (beginIdx !== -1 && endIdx === -1) {
118
+ // self-heal: begin 만 있고 end 손상
119
+ newContent = `${current.slice(0, beginIdx).replace(/\s+$/, '')}\n\n${block}\n`;
120
+ }
121
+ else if (current.length === 0) {
122
+ // 신규 파일
123
+ newContent = `# Agent Instructions (forgen-managed)\n\n${block}\n`;
124
+ }
125
+ else {
126
+ // 기존 파일에 append
127
+ newContent = `${current.replace(/\s+$/, '')}\n\n${block}\n`;
128
+ }
129
+ }
130
+ if (newContent === current)
131
+ return false;
132
+ if (!dryRun) {
133
+ fs.mkdirSync(path.dirname(agentsMdPath), { recursive: true });
134
+ fs.writeFileSync(agentsMdPath, newContent, 'utf-8');
135
+ }
136
+ return true;
137
+ }
package/dist/core/init.js CHANGED
@@ -9,6 +9,7 @@ import * as path from 'node:path';
9
9
  import { profileExists } from '../store/profile-store.js';
10
10
  import { ensureV1Directories } from './v1-bootstrap.js';
11
11
  import { initializeForgenHome } from './init-cli.js';
12
+ import { injectDevGuidePrinciples } from './dev-guide-injector.js';
12
13
  // ── CLI 핸들러 ──
13
14
  export async function handleInit(_args) {
14
15
  const cwd = process.cwd();
@@ -19,6 +20,21 @@ export async function handleInit(_args) {
19
20
  // 프로젝트 .claude/rules 디렉토리 생성
20
21
  const rulesDir = path.join(cwd, '.claude', 'rules');
21
22
  fs.mkdirSync(rulesDir, { recursive: true });
23
+ // dev-guide principles 자동 inject — profile 존재 여부와 무관한 stateless 작업.
24
+ // profileExists() early-return / onboarding 흐름 어디든 동일하게 통과해야 하므로
25
+ // 두 분기 앞쪽에 배치 (기존 위치는 unreachable 경로였음).
26
+ const pkgRoot = path.resolve(import.meta.dirname ?? __dirname, '..', '..');
27
+ const injectResult = injectDevGuidePrinciples({ cwd, pkgRoot });
28
+ if (injectResult.stack) {
29
+ const { side, stack } = injectResult.stack;
30
+ const stackLabel = side === 'fe'
31
+ ? (stack === 'react' ? 'React/Next' : 'Vue/Nuxt')
32
+ : (stack === 'go' ? 'Go' : 'Node.js');
33
+ console.log(` 💡 ${stackLabel} 프로젝트 감지됨. dev-guide principles 자동 inject:`);
34
+ console.log(` ${injectResult.claudeRulePath} (Claude)`);
35
+ console.log(` ${injectResult.agentsMdPath} (Codex, managed block)`);
36
+ suggestStackSkills(cwd);
37
+ }
22
38
  // v0.4.1 (2026-04-24): starter-pack 프로비저닝 — 격리 홈 / 신규 FORGEN_HOME
23
39
  // 에서 "신규 사용자 첫날 가치" 가 0이 되는 결함 해소. npm install-g 시의
24
40
  // postinstall 이 하던 starter 배포를 런타임에서도 보장.
@@ -52,3 +68,40 @@ export async function handleInit(_args) {
52
68
  console.log(' forgen inspect profile View your profile');
53
69
  console.log(' forgen doctor Check system health\n');
54
70
  }
71
+ // 감지된 스택을 알리고, fgx 번들에 포함된 dev-guide 스킬을 안내.
72
+ // 실제 설치는 install-claude.ts 가 ~/.claude/skills/ 에 처리.
73
+ function suggestStackSkills(cwd) {
74
+ const pkgPath = path.join(cwd, 'package.json');
75
+ const goModPath = path.join(cwd, 'go.mod');
76
+ let stack = null;
77
+ let skills = [];
78
+ if (fs.existsSync(pkgPath)) {
79
+ try {
80
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
81
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
82
+ if ('react' in deps || 'next' in deps) {
83
+ stack = 'React/Next';
84
+ skills = ['fe-build-react', 'fe-review-react', 'fe-perf-react'];
85
+ }
86
+ else if ('vue' in deps || 'nuxt' in deps) {
87
+ stack = 'Vue/Nuxt';
88
+ skills = ['fe-build-vue', 'fe-review-vue', 'fe-perf-vue'];
89
+ }
90
+ else {
91
+ stack = 'Node.js';
92
+ skills = ['be-build-node', 'be-review-node', 'be-perf-node', 'be-security-node'];
93
+ }
94
+ }
95
+ catch { /* ignore */ }
96
+ }
97
+ else if (fs.existsSync(goModPath)) {
98
+ stack = 'Go';
99
+ skills = ['be-build-go', 'be-review-go', 'be-perf-go', 'be-security-go'];
100
+ }
101
+ if (!stack)
102
+ return;
103
+ console.log(` 💡 ${stack} 프로젝트로 감지됨. fgx 번들 dev-guide 스킬 사용 가능:`);
104
+ for (const s of skills)
105
+ console.log(` /${s}`);
106
+ console.log('');
107
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Forgen — Lifecycle Classifier (P3)
3
+ *
4
+ * 솔루션 catalog (~/.forgen/me/solutions/*.md) 를 읽어 각 솔루션의
5
+ * lifecycle 을 hot/warm/cold/dead/new 로 분류한다.
6
+ */
7
+ import type { HitRateRow } from './observability-store.js';
8
+ export type Lifecycle = 'hot' | 'warm' | 'cold' | 'dead' | 'new';
9
+ export interface LifecycleClass {
10
+ solutionId: string;
11
+ lifecycle: Lifecycle;
12
+ /** acted_90d / max(surfaced_90d, 1). surfaced_90d == 0 이면 null */
13
+ hitRate: number | null;
14
+ matched_90d: number;
15
+ surfaced_90d: number;
16
+ acted_90d: number;
17
+ matched_180d: number;
18
+ ageDays: number;
19
+ }
20
+ /** 분류 로직 — §5.2 */
21
+ export declare function classifyOne(_solutionId: string, ageDays: number, rates: HitRateRow): Lifecycle;
22
+ /** ~/.forgen/me/solutions/*.md 전체를 분류하여 반환 */
23
+ export declare function classifySolutions(): LifecycleClass[];
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Forgen — Lifecycle Classifier (P3)
3
+ *
4
+ * 솔루션 catalog (~/.forgen/me/solutions/*.md) 를 읽어 각 솔루션의
5
+ * lifecycle 을 hot/warm/cold/dead/new 로 분류한다.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { ME_SOLUTIONS } from './paths.js';
10
+ import { parseFrontmatterOnly } from '../engine/solution-format.js';
11
+ import { queryHitRate } from './observability-store.js';
12
+ /** 분류 로직 — §5.2 */
13
+ export function classifyOne(_solutionId, ageDays, rates) {
14
+ // new: age ≤ 30d
15
+ if (ageDays <= 30)
16
+ return 'new';
17
+ // dead: matched_180d == 0 AND age > 30d
18
+ if (rates.matched_180d === 0)
19
+ return 'dead';
20
+ // hot: acted_90d >= 3 AND (acted_90d / max(surfaced_90d, 1)) >= 0.4
21
+ const hitRate = rates.acted_90d / Math.max(rates.surfaced_90d, 1);
22
+ if (rates.acted_90d >= 3 && hitRate >= 0.4)
23
+ return 'hot';
24
+ // warm: surfaced_90d >= 3 AND acted_90d >= 1
25
+ if (rates.surfaced_90d >= 3 && rates.acted_90d >= 1)
26
+ return 'warm';
27
+ // cold: matched_90d >= 1 AND surfaced_90d == 0
28
+ if (rates.matched_90d >= 1 && rates.surfaced_90d === 0)
29
+ return 'cold';
30
+ // fallback
31
+ return 'cold';
32
+ }
33
+ /** ~/.forgen/me/solutions/*.md 전체를 분류하여 반환 */
34
+ export function classifySolutions() {
35
+ const results = [];
36
+ let files;
37
+ try {
38
+ if (!fs.existsSync(ME_SOLUTIONS))
39
+ return [];
40
+ files = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ // queryHitRate() 는 전체 결과를 한 번에 가져옴
46
+ const rateMap = new Map();
47
+ try {
48
+ const rows = queryHitRate();
49
+ for (const row of rows) {
50
+ rateMap.set(row.solutionId, row);
51
+ }
52
+ }
53
+ catch {
54
+ // fail-open: DB 없으면 빈 map
55
+ }
56
+ const now = Date.now();
57
+ for (const file of files) {
58
+ const filePath = path.join(ME_SOLUTIONS, file);
59
+ let content;
60
+ try {
61
+ content = fs.readFileSync(filePath, 'utf-8');
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ const fm = parseFrontmatterOnly(content);
67
+ if (!fm)
68
+ continue;
69
+ const solutionId = fm.name;
70
+ // ageDays: frontmatter created date 에서 계산
71
+ let ageDays = 999;
72
+ try {
73
+ const createdMs = new Date(fm.created).getTime();
74
+ if (!isNaN(createdMs)) {
75
+ ageDays = Math.floor((now - createdMs) / (24 * 60 * 60 * 1000));
76
+ }
77
+ }
78
+ catch {
79
+ // keep 999 (treat as old)
80
+ }
81
+ const rates = rateMap.get(solutionId) ?? {
82
+ solutionId,
83
+ matched_30d: 0, surfaced_30d: 0, acted_30d: 0,
84
+ matched_90d: 0, surfaced_90d: 0, acted_90d: 0,
85
+ matched_180d: 0, surfaced_180d: 0, acted_180d: 0,
86
+ last_event_ts: 0,
87
+ };
88
+ const lifecycle = classifyOne(solutionId, ageDays, rates);
89
+ const hitRate = rates.surfaced_90d > 0
90
+ ? rates.acted_90d / rates.surfaced_90d
91
+ : null;
92
+ results.push({
93
+ solutionId,
94
+ lifecycle,
95
+ hitRate,
96
+ matched_90d: rates.matched_90d,
97
+ surfaced_90d: rates.surfaced_90d,
98
+ acted_90d: rates.acted_90d,
99
+ matched_180d: rates.matched_180d,
100
+ ageDays,
101
+ });
102
+ }
103
+ return results;
104
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Forgen — Observability Backfill (Phase 2)
3
+ *
4
+ * 기존 JSONL 상태 파일에서 solution_events 를 소급 생성한다.
5
+ * Phase A (결정론적): match-eval-log, implicit-feedback, compound-usage, outcomes
6
+ * Phase B (휴리스틱): transcript 스캔 — CLI --phase B|all 로만 활성
7
+ *
8
+ * 안전성:
9
+ * - 기본: events 가 이미 있으면 reject (--force 필요)
10
+ * - signal_source 에 '-backfill' prefix 로 실시간 emit 과 구분
11
+ * - BEGIN/COMMIT 단위 트랜잭션
12
+ * - fail-open: 파일 누락은 조용히 skip
13
+ */
14
+ export interface BackfillOptions {
15
+ force?: boolean;
16
+ phase?: 'A' | 'B' | 'all';
17
+ dryRun?: boolean;
18
+ }
19
+ export interface BackfillResult {
20
+ phaseA: {
21
+ matched: number;
22
+ surfaced: number;
23
+ acted_on: number;
24
+ };
25
+ phaseB: {
26
+ acted_on: number;
27
+ };
28
+ total: number;
29
+ durationMs: number;
30
+ }
31
+ export declare function runBackfill(opts?: BackfillOptions): Promise<BackfillResult>;