@wooojin/forgen 0.4.8 → 0.4.10

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 (131) 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 +50 -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/doctor.js +37 -1
  98. package/dist/core/git-stats.d.ts +23 -0
  99. package/dist/core/git-stats.js +50 -0
  100. package/dist/core/init.js +53 -0
  101. package/dist/core/inspect-cli.js +60 -1
  102. package/dist/core/lifecycle-classifier.d.ts +23 -0
  103. package/dist/core/lifecycle-classifier.js +104 -0
  104. package/dist/core/observability-backfill.d.ts +31 -0
  105. package/dist/core/observability-backfill.js +178 -0
  106. package/dist/core/observability-store.d.ts +58 -0
  107. package/dist/core/observability-store.js +195 -0
  108. package/dist/core/regress-map-cli.d.ts +7 -0
  109. package/dist/core/regress-map-cli.js +59 -0
  110. package/dist/core/session-store.js +4 -0
  111. package/dist/core/spawn.d.ts +17 -0
  112. package/dist/core/spawn.js +179 -2
  113. package/dist/core/statusline-cli.js +34 -1
  114. package/dist/engine/compound-extractor.js +39 -0
  115. package/dist/engine/compound-loop.js +6 -0
  116. package/dist/engine/compound-retire.d.ts +20 -0
  117. package/dist/engine/compound-retire.js +85 -0
  118. package/dist/fgx.d.ts +4 -1
  119. package/dist/fgx.js +46 -10
  120. package/dist/hooks/context-guard.js +25 -1
  121. package/dist/hooks/post-tool-use.js +48 -0
  122. package/dist/hooks/solution-injector.js +93 -0
  123. package/dist/host/install-claude.d.ts +6 -2
  124. package/dist/host/install-claude.js +74 -2
  125. package/dist/host/install-codex.d.ts +4 -0
  126. package/dist/host/install-codex.js +71 -0
  127. package/dist/host/install-orchestrator.js +1 -0
  128. package/dist/store/evidence-store.js +9 -2
  129. package/package.json +6 -6
  130. package/plugin.json +1 -1
  131. 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
+ }
@@ -491,12 +491,18 @@ export async function runDoctor(opts = {}) {
491
491
  }
492
492
  // P4 셀프 가드: fix:feat 비율 30% 초과 시 회귀 패턴 의심 경고.
493
493
  try {
494
- const { computeFixFeatRatio, formatFixRatio } = await import('./git-stats.js');
494
+ const { computeFixFeatRatio, formatFixRatio, computeRegressMap } = await import('./git-stats.js');
495
495
  const ratio = computeFixFeatRatio();
496
496
  if (ratio.available) {
497
497
  console.log(` ${formatFixRatio(ratio)}`);
498
498
  if (ratio.exceedsThreshold) {
499
499
  console.log(' ⚠ fix:feat 비율이 임계값을 초과했습니다. "이거 고치면 저거 버그난다" 패턴 의심 — 검증 레이어 invariant 점검 권장.');
500
+ const map = computeRegressMap(process.cwd(), 30, 3);
501
+ if (map.available && map.hotspots.length > 0) {
502
+ const top = map.hotspots.map((h) => `${h.path} (${h.fixHits})`).join(', ');
503
+ console.log(` → 진앙 후보 top 3: ${top}`);
504
+ console.log(' → 전체 보기: forgen regress-map');
505
+ }
500
506
  }
501
507
  }
502
508
  }
@@ -529,6 +535,36 @@ export async function runDoctor(opts = {}) {
529
535
  // [Codex Parity] — parity-result.json 신선도 검사 (v0.4.2 패턴 확장)
530
536
  renderCodexParity();
531
537
  console.log();
538
+ // [ψ-long] — within-conversation compound 효과 측정 (v0.4.10)
539
+ // 측정 시간이 길어 ship-gate 는 advisory. 신선도(<= 14d) + psiLong > 0 만 확인.
540
+ console.log(' [ψ-long compound]');
541
+ try {
542
+ const psiLongPath = path.join(STATE_DIR, 'psi-long-result.json');
543
+ if (!fs.existsSync(psiLongPath)) {
544
+ console.log(' ○ no measurement yet — `pnpm --filter @wooojin/forgen-eval psi:long` 권장');
545
+ }
546
+ else {
547
+ const raw = fs.readFileSync(psiLongPath, 'utf-8');
548
+ const data = JSON.parse(raw);
549
+ const ageMs = data.at ? Date.now() - new Date(data.at).getTime() : Number.POSITIVE_INFINITY;
550
+ const ageDays = Math.floor(ageMs / (24 * 60 * 60 * 1000));
551
+ const psiLong = data.psiLong ?? 0;
552
+ const passed = !!data.passed;
553
+ const freshTag = ageDays <= 14 ? 'fresh' : `${ageDays}d stale`;
554
+ const gateLabel = passed ? '✓ PASS' : '✗ FAIL';
555
+ console.log(` ${gateLabel} ψ_long=${psiLong.toFixed(3)} N=${data.N ?? '?'} (${freshTag})`);
556
+ if (!passed) {
557
+ console.log(' ⚠ within-session compound 효과 미관측 — correction injection / rule trigger 경로 점검');
558
+ }
559
+ else if (ageDays > 14) {
560
+ console.log(' △ 측정 14일 초과 — 재측정 권장 (`pnpm --filter @wooojin/forgen-eval psi:long`)');
561
+ }
562
+ }
563
+ }
564
+ catch (e) {
565
+ console.log(` Unable to read psi-long state: ${e instanceof Error ? e.message : 'unknown'}`);
566
+ }
567
+ console.log();
532
568
  // [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
533
569
  console.log(' [Summary]');
534
570
  if (failedChecks.length === 0) {
@@ -34,3 +34,26 @@ export declare function computeFixFeatRatio(cwd?: string, windowSize?: number, t
34
34
  export declare function parseGitLog(rawLog: string, windowSize?: number, threshold?: number): FixRatioStats;
35
35
  /** 사람용 한 줄 라벨. */
36
36
  export declare function formatFixRatio(s: FixRatioStats): string;
37
+ export interface RegressHotspot {
38
+ path: string;
39
+ fixHits: number;
40
+ lastFixSha: string;
41
+ lastFixDate: string;
42
+ }
43
+ export interface RegressMap {
44
+ windowDays: number;
45
+ fixCommits: number;
46
+ hotspots: RegressHotspot[];
47
+ available: boolean;
48
+ }
49
+ /**
50
+ * 지난 N일 동안 fix 커밋이 닿은 파일 hot-list.
51
+ *
52
+ * doctor 가 fix:feat 비율로 "회귀 패턴 의심" 시그널은 주지만, 진앙은 안 보여준다.
53
+ * regress-map 은 fix 커밋의 changed-files 를 카운트해 top-N hotspot 을 추출 →
54
+ * "이거 고치면 저거 깨진다" 의 *저거* 후보를 한 화면에 노출.
55
+ *
56
+ * 제외 scope (fix:feat 와 동일): fix(test|tests|docs|doc) — 노이즈.
57
+ */
58
+ export declare function computeRegressMap(cwd?: string, windowDays?: number, topN?: number): RegressMap;
59
+ export declare function parseRegressLog(raw: string, windowDays: number, topN: number): RegressMap;
@@ -77,3 +77,53 @@ export function formatFixRatio(s) {
77
77
  const flag = s.exceedsThreshold ? ` ⚠ over ${thresholdPct}%` : '';
78
78
  return `fix:feat ratio ${pct}% (${s.fixCount}/${s.fixCount + s.featCount} in last ${s.windowSize})${flag}`;
79
79
  }
80
+ /**
81
+ * 지난 N일 동안 fix 커밋이 닿은 파일 hot-list.
82
+ *
83
+ * doctor 가 fix:feat 비율로 "회귀 패턴 의심" 시그널은 주지만, 진앙은 안 보여준다.
84
+ * regress-map 은 fix 커밋의 changed-files 를 카운트해 top-N hotspot 을 추출 →
85
+ * "이거 고치면 저거 깨진다" 의 *저거* 후보를 한 화면에 노출.
86
+ *
87
+ * 제외 scope (fix:feat 와 동일): fix(test|tests|docs|doc) — 노이즈.
88
+ */
89
+ export function computeRegressMap(cwd = process.cwd(), windowDays = 30, topN = 10) {
90
+ try {
91
+ const since = `--since=${windowDays}.days.ago`;
92
+ const out = execFileSync('git', ['log', '--no-merges', since, '--name-only', '--pretty=format:%x1fCOMMIT%x1f%h%x1f%cs%x1f%s'], { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 });
93
+ return parseRegressLog(out, windowDays, topN);
94
+ }
95
+ catch {
96
+ return { windowDays, fixCommits: 0, hotspots: [], available: false };
97
+ }
98
+ }
99
+ export function parseRegressLog(raw, windowDays, topN) {
100
+ const blocks = raw.split('\x1fCOMMIT\x1f').slice(1);
101
+ const counter = new Map();
102
+ let fixCommits = 0;
103
+ for (const block of blocks) {
104
+ const [sha, date, rest = ''] = block.split('\x1f');
105
+ const nl = rest.indexOf('\n');
106
+ const subject = nl === -1 ? rest : rest.slice(0, nl);
107
+ const files = nl === -1 ? [] : rest.slice(nl + 1).split('\n').map((s) => s.trim()).filter(Boolean);
108
+ const m = subject.match(/^(fix|feat)(?:\(([^)]+)\))?:/);
109
+ if (!m || m[1] !== 'fix')
110
+ continue;
111
+ const scope = (m[2] ?? '').toLowerCase().trim();
112
+ if (SCOPE_EXCLUSIONS.has(scope))
113
+ continue;
114
+ fixCommits++;
115
+ for (const f of files) {
116
+ const existing = counter.get(f);
117
+ if (existing) {
118
+ existing.fixHits++;
119
+ }
120
+ else {
121
+ counter.set(f, { path: f, fixHits: 1, lastFixSha: sha, lastFixDate: date });
122
+ }
123
+ }
124
+ }
125
+ const hotspots = Array.from(counter.values())
126
+ .sort((a, b) => b.fixHits - a.fixHits || a.path.localeCompare(b.path))
127
+ .slice(0, topN);
128
+ return { windowDays, fixCommits, hotspots, available: true };
129
+ }
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
+ }
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { loadProfile } from '../store/profile-store.js';
10
10
  import { loadAllRules, loadActiveRules } from '../store/rule-store.js';
11
- import { loadRecentEvidence } from '../store/evidence-store.js';
11
+ import { loadRecentEvidence, loadAllEvidence } from '../store/evidence-store.js';
12
12
  import { loadRecentSessions } from '../store/session-state-store.js';
13
13
  import * as inspect from '../renderer/inspect-renderer.js';
14
14
  import { ME_BEHAVIOR, ME_SOLUTIONS, STATE_DIR } from './paths.js';
@@ -71,6 +71,10 @@ export async function handleInspect(args) {
71
71
  }
72
72
  console.log('');
73
73
  }
74
+ // ── Receipts: correction → rule → inject ── (v0.4.10)
75
+ // "내가 forgen 으로 무엇을 얻었나" 한 화면. correction 이 만들어낸 rule 의 lifecycle 을
76
+ // 묶어 표시 — 주입(inject_count), 마지막 주입 시각, host 분포.
77
+ await renderReceipts();
74
78
  return;
75
79
  }
76
80
  if (sub === 'rules') {
@@ -157,3 +161,58 @@ export async function handleInspect(args) {
157
161
  forgen inspect bypass [--last N] — 사용자 우회 기록
158
162
  forgen inspect drift [--last N] — stuck-loop force-approve 기록`);
159
163
  }
164
+ /**
165
+ * v0.4.10 Receipts: correction → rule → inject 트리플.
166
+ *
167
+ * 사용자 가치 명제("the more you use, the better it knows") 의 receipt.
168
+ * 1) Top-injected rules (실제로 Claude 에 주입된 룰 N회)
169
+ * 2) Recent rule births (correction → 새 룰 생성된 이력)
170
+ * 3) Host 균형 (claude vs codex)
171
+ */
172
+ async function renderReceipts() {
173
+ const rules = loadAllRules();
174
+ const evidence = loadAllEvidence();
175
+ const injected = rules
176
+ .filter((r) => r.lifecycle && (r.lifecycle.inject_count ?? 0) > 0)
177
+ .sort((a, b) => (b.lifecycle?.inject_count ?? 0) - (a.lifecycle?.inject_count ?? 0))
178
+ .slice(0, 5);
179
+ console.log('── Receipts (correction → rule → inject) ──');
180
+ if (injected.length === 0) {
181
+ console.log(' No injected rules yet. Trigger a session to start the loop.');
182
+ }
183
+ else {
184
+ console.log(' Top injected rules:');
185
+ for (const r of injected) {
186
+ const count = r.lifecycle?.inject_count ?? 0;
187
+ const last = (r.lifecycle?.last_inject_at ?? '').slice(0, 10) || 'never';
188
+ const trigger = (r.trigger ?? '').slice(0, 48);
189
+ console.log(` ${String(count).padStart(3)}× [${r.category}] ${trigger} (last ${last})`);
190
+ }
191
+ }
192
+ // Recent rule births: 마지막 7일 내 created_at 인 룰 (T = correction → rule)
193
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
194
+ const recentRules = rules
195
+ .filter((r) => new Date(r.created_at).getTime() >= sevenDaysAgo)
196
+ .sort((a, b) => b.created_at.localeCompare(a.created_at))
197
+ .slice(0, 3);
198
+ if (recentRules.length > 0) {
199
+ console.log(' New rules (last 7 days):');
200
+ for (const r of recentRules) {
201
+ const dateStr = r.created_at.slice(0, 10);
202
+ console.log(` + [${r.category}/${r.strength}] ${(r.trigger ?? '').slice(0, 48)} (${dateStr})`);
203
+ }
204
+ }
205
+ // Host 균형 — multi-host 가치 명제의 receipt
206
+ const hostCount = { claude: 0, codex: 0 };
207
+ for (const e of evidence) {
208
+ const h = (e.host ?? 'claude');
209
+ hostCount[h] = (hostCount[h] ?? 0) + 1;
210
+ }
211
+ const total = hostCount.claude + hostCount.codex;
212
+ if (total > 0) {
213
+ const cpct = Math.round((hostCount.claude / total) * 100);
214
+ const xpct = 100 - cpct;
215
+ console.log(` Host balance: claude ${cpct}% · codex ${xpct}% (n=${total})`);
216
+ }
217
+ console.log('');
218
+ }