@wooojin/forgen 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +94 -0
  3. package/README.ja.md +119 -8
  4. package/README.ko.md +73 -2
  5. package/README.md +163 -9
  6. package/README.zh.md +87 -7
  7. package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
  8. package/dist/checks/conclusion-verification-ratio.js +86 -0
  9. package/dist/checks/fact-vs-agreement.d.ts +47 -0
  10. package/dist/checks/fact-vs-agreement.js +92 -0
  11. package/dist/checks/self-score-deflation.d.ts +38 -0
  12. package/dist/checks/self-score-deflation.js +108 -0
  13. package/dist/cli.js +158 -6
  14. package/dist/core/auto-compound-runner.js +85 -13
  15. package/dist/core/dashboard.js +9 -2
  16. package/dist/core/doctor.js +90 -15
  17. package/dist/core/extraction-notice.d.ts +18 -0
  18. package/dist/core/extraction-notice.js +64 -0
  19. package/dist/core/init-cli.d.ts +26 -0
  20. package/dist/core/init-cli.js +104 -0
  21. package/dist/core/init.js +17 -0
  22. package/dist/core/inspect-cli.js +64 -5
  23. package/dist/core/migrate-cli.d.ts +10 -0
  24. package/dist/core/migrate-cli.js +34 -0
  25. package/dist/core/paths.d.ts +8 -1
  26. package/dist/core/paths.js +11 -2
  27. package/dist/core/recall-cli.d.ts +26 -0
  28. package/dist/core/recall-cli.js +125 -0
  29. package/dist/core/recall-reference-detector.d.ts +43 -0
  30. package/dist/core/recall-reference-detector.js +65 -0
  31. package/dist/core/state-gc.d.ts +19 -0
  32. package/dist/core/state-gc.js +48 -4
  33. package/dist/core/stats-cli.d.ts +36 -0
  34. package/dist/core/stats-cli.js +254 -0
  35. package/dist/core/uninstall.d.ts +1 -0
  36. package/dist/core/uninstall.js +25 -1
  37. package/dist/core/v1-bootstrap.js +9 -1
  38. package/dist/engine/classify-enforce-cli.d.ts +8 -0
  39. package/dist/engine/classify-enforce-cli.js +61 -0
  40. package/dist/engine/compound-cli.js +1 -0
  41. package/dist/engine/compound-export.js +8 -3
  42. package/dist/engine/enforce-classifier.d.ts +31 -0
  43. package/dist/engine/enforce-classifier.js +123 -0
  44. package/dist/engine/learn-cli.js +1 -4
  45. package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
  46. package/dist/engine/lifecycle/bypass-detector.js +82 -0
  47. package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
  48. package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
  49. package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
  50. package/dist/engine/lifecycle/meta-cli.js +7 -0
  51. package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
  52. package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
  53. package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
  54. package/dist/engine/lifecycle/orchestrator.js +131 -0
  55. package/dist/engine/lifecycle/signals.d.ts +30 -0
  56. package/dist/engine/lifecycle/signals.js +142 -0
  57. package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
  58. package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
  59. package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
  60. package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
  61. package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
  62. package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
  63. package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
  64. package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
  65. package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
  66. package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
  67. package/dist/engine/lifecycle/types.d.ts +52 -0
  68. package/dist/engine/lifecycle/types.js +7 -0
  69. package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
  70. package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
  71. package/dist/engine/rule-toggle-cli.d.ts +13 -0
  72. package/dist/engine/rule-toggle-cli.js +76 -0
  73. package/dist/engine/skill-promoter.js +3 -6
  74. package/dist/forge/evidence-processor.js +10 -2
  75. package/dist/hooks/context-guard.js +72 -1
  76. package/dist/hooks/dangerous-patterns.json +3 -3
  77. package/dist/hooks/db-guard.js +18 -2
  78. package/dist/hooks/intent-classifier.js +1 -1
  79. package/dist/hooks/keyword-detector.js +1 -1
  80. package/dist/hooks/notepad-injector.js +1 -1
  81. package/dist/hooks/permission-handler.js +1 -1
  82. package/dist/hooks/post-tool-failure.js +1 -1
  83. package/dist/hooks/post-tool-use.d.ts +6 -0
  84. package/dist/hooks/post-tool-use.js +94 -14
  85. package/dist/hooks/pre-compact.js +1 -1
  86. package/dist/hooks/pre-tool-use.d.ts +7 -0
  87. package/dist/hooks/pre-tool-use.js +79 -5
  88. package/dist/hooks/rate-limiter.js +1 -1
  89. package/dist/hooks/secret-filter.d.ts +10 -0
  90. package/dist/hooks/secret-filter.js +21 -1
  91. package/dist/hooks/session-recovery.js +1 -1
  92. package/dist/hooks/shared/atomic-write.d.ts +8 -1
  93. package/dist/hooks/shared/atomic-write.js +17 -3
  94. package/dist/hooks/shared/command-parser.d.ts +44 -0
  95. package/dist/hooks/shared/command-parser.js +50 -0
  96. package/dist/hooks/shared/hook-response.d.ts +23 -2
  97. package/dist/hooks/shared/hook-response.js +48 -3
  98. package/dist/hooks/shared/safe-regex.d.ts +25 -0
  99. package/dist/hooks/shared/safe-regex.js +50 -0
  100. package/dist/hooks/shared/stop-triggers.d.ts +19 -0
  101. package/dist/hooks/shared/stop-triggers.js +19 -0
  102. package/dist/hooks/skill-injector.js +1 -1
  103. package/dist/hooks/slop-detector.js +2 -2
  104. package/dist/hooks/solution-injector.d.ts +9 -0
  105. package/dist/hooks/solution-injector.js +48 -5
  106. package/dist/hooks/stop-guard.d.ts +84 -0
  107. package/dist/hooks/stop-guard.js +606 -0
  108. package/dist/hooks/subagent-tracker.js +1 -1
  109. package/dist/i18n/index.js +3 -5
  110. package/dist/mcp/tools.js +19 -2
  111. package/dist/store/evidence-store.d.ts +15 -0
  112. package/dist/store/evidence-store.js +61 -1
  113. package/dist/store/implicit-feedback-store.d.ts +59 -0
  114. package/dist/store/implicit-feedback-store.js +153 -0
  115. package/dist/store/rule-lifecycle.d.ts +23 -0
  116. package/dist/store/rule-lifecycle.js +63 -0
  117. package/dist/store/rule-store.d.ts +21 -0
  118. package/dist/store/rule-store.js +136 -8
  119. package/dist/store/types.d.ts +83 -0
  120. package/dist/store/types.js +7 -1
  121. package/hooks/hook-registry.json +1 -0
  122. package/hooks/hooks.json +6 -1
  123. package/package.json +11 -3
  124. package/plugin.json +1 -1
package/dist/cli.js CHANGED
@@ -169,13 +169,157 @@ const commands = [
169
169
  // 수동 재설치: node scripts/postinstall.js
170
170
  {
171
171
  name: 'uninstall',
172
- description: 'Remove forgen from settings [--force]',
172
+ description: 'Remove forgen from settings [--force] [--purge (also deletes ~/.forgen/)]',
173
173
  handler: async (args) => {
174
174
  const { handleUninstall } = await import('./core/uninstall.js');
175
- await handleUninstall(process.cwd(), { force: args.includes('--force') });
175
+ await handleUninstall(process.cwd(), {
176
+ force: args.includes('--force'),
177
+ purge: args.includes('--purge'),
178
+ });
179
+ },
180
+ },
181
+ {
182
+ name: 'rule',
183
+ description: 'Rule management (list|suppress|activate|scan|health-scan|classify)',
184
+ handler: async (args) => {
185
+ await handleRuleNamespace(args);
186
+ },
187
+ },
188
+ {
189
+ name: 'classify-enforce',
190
+ aliases: ['rule-classify'],
191
+ description: '[alias: rule classify] Propose enforce_via for rules (ADR-001 migration).',
192
+ handler: async (args) => {
193
+ const { handleClassifyEnforce } = await import('./engine/classify-enforce-cli.js');
194
+ await handleClassifyEnforce(args);
195
+ },
196
+ },
197
+ {
198
+ name: 'rule-meta-scan',
199
+ description: '[alias: rule health-scan] Scan drift for stuck-loop events and demote Mech-A rules.',
200
+ handler: async (args) => {
201
+ const { handleRuleMetaScan } = await import('./engine/lifecycle/meta-cli.js');
202
+ await handleRuleMetaScan(args);
203
+ },
204
+ },
205
+ {
206
+ name: 'lifecycle-scan',
207
+ description: '[alias: rule scan] Run all rule lifecycle triggers (T1~T5 + Meta).',
208
+ handler: async (args) => {
209
+ const { handleLifecycleScan } = await import('./engine/lifecycle/lifecycle-cli.js');
210
+ await handleLifecycleScan(args);
211
+ },
212
+ },
213
+ {
214
+ name: 'stats',
215
+ description: 'One-screen dashboard: active rules, corrections, blocks/bypass/drift (7d).',
216
+ handler: async (args) => {
217
+ const { handleStats } = await import('./core/stats-cli.js');
218
+ await handleStats(args);
219
+ },
220
+ },
221
+ {
222
+ name: 'last-block',
223
+ description: 'Show the most recent Mech-A/B block event with rule detail (R6-UX2).',
224
+ handler: async (_args) => {
225
+ const { handleInspect } = await import('./core/inspect-cli.js');
226
+ await handleInspect(['violations', '--last', '1']);
227
+ },
228
+ },
229
+ {
230
+ name: 'recall',
231
+ description: 'Show recent compound recalls (matched solutions) with optional body preview.',
232
+ handler: async (args) => {
233
+ const { handleRecall } = await import('./core/recall-cli.js');
234
+ await handleRecall(args);
235
+ },
236
+ },
237
+ {
238
+ name: 'migrate',
239
+ description: 'One-shot schema migrations (implicit-feedback category backfill).',
240
+ handler: async (args) => {
241
+ const { handleMigrate } = await import('./core/migrate-cli.js');
242
+ await handleMigrate(args);
243
+ },
244
+ },
245
+ {
246
+ name: 'suppress-rule',
247
+ description: '[alias: rule suppress] Disable a rule by id/prefix. Hard rules refused.',
248
+ handler: async (args) => {
249
+ const { handleSuppressRule } = await import('./engine/rule-toggle-cli.js');
250
+ await handleSuppressRule(args);
251
+ },
252
+ },
253
+ {
254
+ name: 'activate-rule',
255
+ description: '[alias: rule activate] Re-activate a suppressed rule by id/prefix.',
256
+ handler: async (args) => {
257
+ const { handleActivateRule } = await import('./engine/rule-toggle-cli.js');
258
+ await handleActivateRule(args);
176
259
  },
177
260
  },
178
261
  ];
262
+ // ---------------------------------------------------------------------------
263
+ // `forgen rule <subcommand>` — user-facing namespace (R9-IA1)
264
+ // Thin dispatcher that routes to existing handlers. Top-level legacy commands
265
+ // (suppress-rule, activate-rule, lifecycle-scan, rule-meta-scan, classify-enforce)
266
+ // remain as backward-compatible aliases.
267
+ // ---------------------------------------------------------------------------
268
+ async function handleRuleNamespace(args) {
269
+ const sub = args[0];
270
+ const rest = args.slice(1);
271
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
272
+ console.log(`
273
+ forgen rule — manage personalization rules
274
+
275
+ Usage:
276
+ forgen rule list List all rules (alias: inspect rules)
277
+ forgen rule suppress <id-or-prefix> Disable a rule (hard rules refused)
278
+ forgen rule activate <id-or-prefix> Re-activate a suppressed rule
279
+ forgen rule scan [--apply] Run lifecycle triggers (promote/demote/retire)
280
+ forgen rule health-scan [--apply] Scan drift → Mech downgrade candidates
281
+ forgen rule classify [--apply] [--force]
282
+ Propose enforce_via for legacy rules
283
+ `);
284
+ return;
285
+ }
286
+ switch (sub) {
287
+ case 'list': {
288
+ const { handleInspect } = await import('./core/inspect-cli.js');
289
+ await handleInspect(['rules', ...rest]);
290
+ return;
291
+ }
292
+ case 'suppress': {
293
+ const { handleSuppressRule } = await import('./engine/rule-toggle-cli.js');
294
+ await handleSuppressRule(rest);
295
+ return;
296
+ }
297
+ case 'activate': {
298
+ const { handleActivateRule } = await import('./engine/rule-toggle-cli.js');
299
+ await handleActivateRule(rest);
300
+ return;
301
+ }
302
+ case 'scan': {
303
+ const { handleLifecycleScan } = await import('./engine/lifecycle/lifecycle-cli.js');
304
+ await handleLifecycleScan(rest);
305
+ return;
306
+ }
307
+ case 'health-scan': {
308
+ const { handleRuleMetaScan } = await import('./engine/lifecycle/meta-cli.js');
309
+ await handleRuleMetaScan(rest);
310
+ return;
311
+ }
312
+ case 'classify': {
313
+ const { handleClassifyEnforce } = await import('./engine/classify-enforce-cli.js');
314
+ await handleClassifyEnforce(rest);
315
+ return;
316
+ }
317
+ default: {
318
+ console.error(`[forgen] Unknown rule subcommand: ${sub}\n Run "forgen rule help" for options.`);
319
+ process.exit(1);
320
+ }
321
+ }
322
+ }
179
323
  /** 최소 편집 거리 (유사 명령 제안용) */
180
324
  function levenshtein(a, b) {
181
325
  const m = a.length, n = b.length;
@@ -298,17 +442,25 @@ function printHelp() {
298
442
  Commands:
299
443
  forgen forge Personalize your coding profile
300
444
  forgen onboarding Run 2-question onboarding
301
- forgen inspect [profile|rules|evidence|session]
302
- Inspect v1 state
445
+ forgen inspect [profile|rules|corrections|session]
446
+ Inspect v1 state (alias: evidence → corrections)
447
+ forgen rule <list|suppress|activate|scan|health-scan|classify>
448
+ Rule management (see: forgen rule help)
449
+ forgen stats One-screen trust-layer dashboard (+ philosophy)
450
+ forgen last-block Show the most recent block event
451
+ forgen recall [--limit N] [--show]
452
+ 최근 compound 주입 이력 (solution body preview)
453
+ forgen migrate [implicit-feedback|all]
454
+ One-shot schema migration (category backfill)
303
455
  forgen compound Manage accumulated knowledge
304
456
  forgen dashboard Compound system dashboard
305
457
  forgen me Personal dashboard
306
- forgen init Initialize project
458
+ forgen init Initialize project (+ starter-pack solutions)
307
459
  forgen config hooks Hook management
308
460
  forgen mcp MCP server management
309
461
  forgen skill promote|list Skill management
310
462
  forgen notepad show|add|clear Session notepad
311
- forgen doctor System diagnostics
463
+ forgen doctor [--prune-state] System diagnostics (+ daily T4 decay on prune)
312
464
  forgen uninstall Remove forgen
313
465
 
314
466
  Harness mode (default):
@@ -11,11 +11,12 @@
11
11
  */
12
12
  import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
14
- import * as os from 'node:os';
15
14
  import { execFileSync } from 'node:child_process';
16
15
  import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
16
+ import { redactSecrets } from '../hooks/secret-filter.js';
17
17
  import { createEvidence, saveEvidence, promoteSessionCandidates } from '../store/evidence-store.js';
18
18
  import { loadProfile } from '../store/profile-store.js';
19
+ import { FORGEN_HOME, ME_DIR } from './paths.js';
19
20
  /** Auto-compound에 사용할 모델 — background 추출이므로 haiku로 충분 */
20
21
  const COMPOUND_MODEL = 'haiku';
21
22
  /** execFileSync wrapper: transient 에러(ETIMEDOUT 등) 시 1회 재시도 */
@@ -48,9 +49,8 @@ const [, , cwd, transcriptPath, sessionId] = process.argv;
48
49
  if (!cwd || !transcriptPath || !sessionId) {
49
50
  process.exit(1);
50
51
  }
51
- const FORGEN_HOME = path.join(os.homedir(), '.forgen');
52
- const SOLUTIONS_DIR = path.join(FORGEN_HOME, 'me', 'solutions');
53
- const BEHAVIOR_DIR = path.join(FORGEN_HOME, 'me', 'behavior');
52
+ const SOLUTIONS_DIR = path.join(ME_DIR, 'solutions');
53
+ const BEHAVIOR_DIR = path.join(ME_DIR, 'behavior');
54
54
  /** Lightweight quality gate for auto-extracted solution files */
55
55
  /** Toxicity patterns — code-context only to avoid false positives on prose */
56
56
  const SOLUTION_TOXICITY_PATTERNS = [/@ts-ignore/i, /:\s*any\b/, /\/\/\s*TODO\b/];
@@ -212,9 +212,16 @@ function mergeOrCreateBehavior(dir, newContent, kind, today) {
212
212
  return false;
213
213
  }
214
214
  try {
215
- const summary = extractSummary(transcriptPath);
216
- if (summary.length < 200)
215
+ const rawSummary = extractSummary(transcriptPath);
216
+ if (rawSummary.length < 200)
217
217
  process.exit(0);
218
+ // R5-G2 (P0 security): transcript 를 Claude 로 송신하기 전 API key / 토큰 / 비밀번호 /
219
+ // private key blocks 를 [REDACTED:...] 로 치환. 사용자가 채팅에 pasted 한 자격증명이
220
+ // auto-compound 를 통해 외부 API 로 누출되는 채널 차단.
221
+ const { redacted: summary, hits: secretHits } = redactSecrets(rawSummary);
222
+ if (secretHits.length > 0) {
223
+ process.stderr.write(`[forgen-auto-compound] redacted ${secretHits.length} secret(s) before send: ${secretHits.map((s) => s.name).join(', ')}\n`);
224
+ }
218
225
  // 보안: 프롬프트 인젝션이 포함된 transcript는 분석하지 않음
219
226
  if (containsPromptInjection(summary)) {
220
227
  process.exit(0);
@@ -375,10 +382,8 @@ ${sanitizedSummary.slice(0, 4000)}
375
382
  }
376
383
  // 3단계: 세션 학습 요약 (SessionLearningSummary) 생성
377
384
  try {
378
- const FORGEN_HOME = path.join(os.homedir(), '.forgen');
379
- const V1_ME_DIR = path.join(FORGEN_HOME, 'me');
380
- const V1_PROFILE = path.join(V1_ME_DIR, 'forge-profile.json');
381
- const V1_EVIDENCE_DIR = path.join(V1_ME_DIR, 'behavior');
385
+ const V1_PROFILE = path.join(ME_DIR, 'forge-profile.json');
386
+ const V1_EVIDENCE_DIR = path.join(ME_DIR, 'behavior');
382
387
  if (fs.existsSync(V1_PROFILE)) {
383
388
  const currentProfile = loadProfile();
384
389
  let profileContext = '';
@@ -477,8 +482,9 @@ ${sanitizedSummary.slice(0, 4000)}
477
482
  process.stderr.write(`[forgen-auto-compound] session learning: ${e instanceof Error ? e.message : String(e)}\n`);
478
483
  }
479
484
  // Step 4: prefer-from-now / avoid-this 교정 → scope:'me' 영구 규칙 승격
485
+ let promotedCount = 0;
480
486
  try {
481
- const promotedCount = promoteSessionCandidates(sessionId);
487
+ promotedCount = promoteSessionCandidates(sessionId);
482
488
  if (promotedCount > 0) {
483
489
  process.stderr.write(`[forgen-auto-compound] promoted ${promotedCount} correction(s) to permanent rules\n`);
484
490
  }
@@ -486,6 +492,21 @@ ${sanitizedSummary.slice(0, 4000)}
486
492
  catch (e) {
487
493
  process.stderr.write(`[forgen-auto-compound] rule promotion: ${e instanceof Error ? e.message : String(e)}\n`);
488
494
  }
495
+ // H2: count newly extracted solutions (post-quality-gate) for Stop hook 알림.
496
+ // solutionsBefore 스냅샷 vs 현재 디스크 상태 차분 → "N개 패턴 학습됨" 1줄.
497
+ let extractedSolutionsCount = 0;
498
+ try {
499
+ if (fs.existsSync(SOLUTIONS_DIR)) {
500
+ const current = fs.readdirSync(SOLUTIONS_DIR).filter((f) => f.endsWith('.md'));
501
+ for (const f of current) {
502
+ if (!solutionsBefore.has(f))
503
+ extractedSolutionsCount++;
504
+ }
505
+ }
506
+ }
507
+ catch (e) {
508
+ process.stderr.write(`[forgen-auto-compound] solution count failed: ${e instanceof Error ? e.message : String(e)}\n`);
509
+ }
489
510
  // Step 5: meta-learning (HyperAgents-inspired self-tuning)
490
511
  try {
491
512
  const { runMetaLearning } = await import('../engine/meta-learning/runner.js');
@@ -500,10 +521,61 @@ ${sanitizedSummary.slice(0, 4000)}
500
521
  catch (e) {
501
522
  process.stderr.write(`[forgen-meta] ${e instanceof Error ? e.message : String(e)}\n`);
502
523
  }
503
- // 완료 기록
524
+ // Step 5.5 (v0.4.1): state hygiene — 세션 스코프 ephemeral 파일 7일 retention
525
+ // 자동 정리. 이전에는 `forgen doctor --prune-state` 수동만 있어서 injection-cache
526
+ // 2343 / modified-files 431 처럼 수천 파일 누적. 몇 달 사용하면 10만+ 파일 → stat
527
+ // 호출 느려지고 디스크 낭비. auto-compound 마다 호출되면 자연스레 정돈.
528
+ try {
529
+ const { pruneState } = await import('./state-gc.js');
530
+ const report = pruneState({ dryRun: false });
531
+ if (report.pruned > 0) {
532
+ const mb = (report.bytesFreed / 1024 / 1024).toFixed(2);
533
+ process.stderr.write(`[forgen-gc] pruned ${report.pruned} stale state files (${mb} MB freed)\n`);
534
+ }
535
+ }
536
+ catch (e) {
537
+ process.stderr.write(`[forgen-gc] state prune failed: ${e instanceof Error ? e.message : String(e)}\n`);
538
+ }
539
+ // Step 6 (v0.4.1): rule lifecycle 자동 실행 — rule 의 violations/bypass/drift
540
+ // 신호에 따른 자동 강등/승격. 이전에는 CLI (`forgen rule scan --apply`) 수동
541
+ // 호출만 있어서 구매자가 몇 주 써도 rule 정비 안 됨 → 쓸모없는 rule 이 계속
542
+ // active. 판매 관점 심각한 "자동 학습 단절". auto-compound-runner 끝에 자동
543
+ // 실행해 세션마다 rule 품질 유지.
544
+ try {
545
+ const { handleLifecycleScan } = await import('../engine/lifecycle/lifecycle-cli.js');
546
+ // silent mode 로 돌리기 위해 stdout 을 임시 리다이렉트 (내부가 console.log 씀)
547
+ const origLog = console.log;
548
+ let applied = 0;
549
+ console.log = (...args) => {
550
+ const msg = args.join(' ');
551
+ const match = msg.match(/apply(?:ied)?\s+(\d+)/i);
552
+ if (match)
553
+ applied = Number(match[1]);
554
+ };
555
+ try {
556
+ await handleLifecycleScan(['--apply']);
557
+ }
558
+ finally {
559
+ console.log = origLog;
560
+ }
561
+ if (applied > 0) {
562
+ process.stderr.write(`[forgen-meta] rule lifecycle: ${applied} event(s) applied\n`);
563
+ }
564
+ }
565
+ catch (e) {
566
+ process.stderr.write(`[forgen-meta] lifecycle scan failed: ${e instanceof Error ? e.message : String(e)}\n`);
567
+ }
568
+ // 완료 기록 — H2: Stop hook 알림용으로 extractedSolutions / promotedRules 포함.
569
+ // noticeShown=false 로 시작해서 Stop hook 가 최초 1회만 surface.
504
570
  const statePath = path.join(FORGEN_HOME, 'state', 'last-auto-compound.json');
505
571
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
506
- fs.writeFileSync(statePath, JSON.stringify({ sessionId, completedAt: new Date().toISOString() }));
572
+ fs.writeFileSync(statePath, JSON.stringify({
573
+ sessionId,
574
+ completedAt: new Date().toISOString(),
575
+ extractedSolutions: extractedSolutionsCount,
576
+ promotedRules: promotedCount,
577
+ noticeShown: false,
578
+ }));
507
579
  }
508
580
  catch (e) {
509
581
  process.stderr.write(`[forgen-auto-compound] ${e instanceof Error ? e.message : String(e)}\n`);
@@ -372,9 +372,15 @@ export function collectLearningCurve() {
372
372
  const files = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.json'));
373
373
  for (const f of files) {
374
374
  try {
375
+ // v0.4.1 정확도 수정: "교정 추이" 라벨은 explicit_correction evidence 만 포함.
376
+ // 이전에는 behavior_observation + session_summary 까지 전부 "교정" 으로
377
+ // 카운트되어 실측 488건 중 ~1건만 실제 교정인데 신뢰도 훼손. axis_hint 는
378
+ // raw_payload 에도 저장되므로 fallback 체크.
375
379
  const data = JSON.parse(fs.readFileSync(path.join(ME_BEHAVIOR, f), 'utf-8'));
376
380
  if (!data.timestamp)
377
381
  continue;
382
+ if (data.type && data.type !== 'explicit_correction')
383
+ continue;
378
384
  const ts = new Date(data.timestamp).getTime();
379
385
  if (!Number.isFinite(ts))
380
386
  continue;
@@ -383,8 +389,9 @@ export function collectLearningCurve() {
383
389
  correctionsLast7d++;
384
390
  else if (age < 2 * SEVEN_DAYS_MS)
385
391
  correctionsPrev7d++;
386
- if (data.axis_hint) {
387
- axisCounts.set(data.axis_hint, (axisCounts.get(data.axis_hint) ?? 0) + 1);
392
+ const axisHint = data.axis_hint ?? data.raw_payload?.axis_hint;
393
+ if (axisHint) {
394
+ axisCounts.set(axisHint, (axisCounts.get(axisHint) ?? 0) + 1);
388
395
  }
389
396
  uniqueDays.add(new Date(ts).toISOString().slice(0, 10));
390
397
  }
@@ -2,15 +2,24 @@ import * as fs from 'node:fs';
2
2
  import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
4
  import { execFileSync } from 'node:child_process';
5
- import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_PHILOSOPHY, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
5
+ import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
6
6
  import { getTimingStats } from '../hooks/shared/hook-timing.js';
7
7
  import { countSessionScopedFiles, pruneState } from './state-gc.js';
8
8
  /** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
9
9
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
10
+ let currentSection = '';
11
+ let failedChecks = [];
12
+ function section(name) {
13
+ currentSection = name;
14
+ console.log(` [${name}]`);
15
+ }
10
16
  function check(label, condition, hint) {
11
17
  const icon = condition ? '✓' : '✗';
12
18
  const hintStr = !condition && hint ? ` — ${hint}` : '';
13
19
  console.log(` ${icon} ${label}${hintStr}`);
20
+ if (!condition) {
21
+ failedChecks.push({ section: currentSection, label, hint });
22
+ }
14
23
  }
15
24
  function exists(p) {
16
25
  return fs.existsSync(p);
@@ -26,14 +35,15 @@ function commandExists(cmd) {
26
35
  }
27
36
  }
28
37
  export async function runDoctor(opts = {}) {
38
+ failedChecks = [];
29
39
  console.log('\n Forgen — Diagnostics\n');
30
- console.log(' [Tools]');
40
+ section('Tools');
31
41
  check('claude CLI', commandExists('claude'));
32
42
  check('tmux', commandExists('tmux'));
33
43
  check('git', commandExists('git'));
34
44
  check('gh (GitHub CLI)', commandExists('gh'), 'Required for team PR features: brew install gh');
35
45
  console.log();
36
- console.log(' [Plugins]');
46
+ section('Plugins');
37
47
  const ralphLoopInstalled = exists(path.join(os.homedir(), '.claude', 'plugins', 'cache', 'claude-plugins-official', 'ralph-loop'));
38
48
  check('ralph-loop plugin', ralphLoopInstalled, 'Required for ralph mode auto-iteration. Install: claude plugins install ralph-loop');
39
49
  // forgen 플러그인 캐시 디렉토리 확인 — 훅 실행의 필수 전제
@@ -68,7 +78,7 @@ export async function runDoctor(opts = {}) {
68
78
  }
69
79
  check('forgen plugin registered & installPath exists', pluginRegistered, 'Plugin registered but installPath missing on disk. Fix: npm run build && node scripts/postinstall.js');
70
80
  console.log();
71
- console.log(' [Directories]');
81
+ section('Directories');
72
82
  check('~/.forgen/', exists(FORGEN_HOME));
73
83
  check('~/.forgen/me/', exists(ME_DIR));
74
84
  check('~/.forgen/me/solutions/', exists(ME_SOLUTIONS));
@@ -76,26 +86,58 @@ export async function runDoctor(opts = {}) {
76
86
  check('~/.forgen/me/rules/', exists(ME_RULES));
77
87
  check('~/.forgen/packs/', exists(PACKS_DIR));
78
88
  check('~/.forgen/sessions/', exists(SESSIONS_DIR));
89
+ // R9-IA5: warn if a user dropped rule files at ~/.forgen/rules/ by mistake.
90
+ // That path is NOT loaded — personal rules live at ~/.forgen/me/rules/.
91
+ const legacyRulesPath = path.join(FORGEN_HOME, 'rules');
92
+ if (exists(legacyRulesPath) && legacyRulesPath !== ME_RULES) {
93
+ try {
94
+ const files = fs.readdirSync(legacyRulesPath).filter((f) => f.endsWith('.json'));
95
+ if (files.length > 0) {
96
+ check(`~/.forgen/rules/ (${files.length} orphan file(s))`, false, `This path is NOT loaded. Move files to ~/.forgen/me/rules/ or delete them.`);
97
+ }
98
+ }
99
+ catch {
100
+ // permission / symlink issue — diagnostics must not crash
101
+ }
102
+ }
79
103
  console.log();
80
- console.log(' [Philosophy]');
81
- check('philosophy.json', exists(ME_PHILOSOPHY));
82
- console.log();
83
- console.log(' [Environment]');
84
- check('Inside tmux session', !!process.env.TMUX);
85
- check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1');
104
+ section('Environment');
105
+ check('Inside tmux session', !!process.env.TMUX, 'FORGEN auto-compound relies on tmux. Launch: tmux new -s forgen');
106
+ check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1', 'Set by `forgen` / `fgx` launcher. Hooks assume harness mode is active.');
86
107
  console.log();
87
- // 솔루션/규칙
108
+ // v0.4.1 파일 확장자 버그 수정: rules 는 .json, behavior 도 대부분 .json 포맷.
109
+ // 이전에 .md 만 count 해서 실 rules 4개인데 0 으로 표시되는 incident 관찰.
110
+ // (compound-export countFiles 와 동일 결함 — 일관된 수정).
111
+ const isKnowledgeFile = (f) => f.endsWith('.md') || f.endsWith('.json');
88
112
  if (exists(ME_SOLUTIONS)) {
89
- const solutions = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md')).length;
113
+ const solutions = fs.readdirSync(ME_SOLUTIONS).filter(isKnowledgeFile).length;
90
114
  console.log(` Personal solutions: ${solutions}`);
91
115
  }
92
116
  if (exists(ME_BEHAVIOR)) {
93
- const behavior = fs.readdirSync(ME_BEHAVIOR).filter((f) => f.endsWith('.md')).length;
117
+ const behavior = fs.readdirSync(ME_BEHAVIOR).filter(isKnowledgeFile).length;
94
118
  console.log(` Behavioral patterns: ${behavior}`);
95
119
  }
96
120
  if (exists(ME_RULES)) {
97
- const rules = fs.readdirSync(ME_RULES).filter((f) => f.endsWith('.md')).length;
98
- console.log(` Personal rules: ${rules}`);
121
+ // v0.4.1 정확도: removed 상태 rule 은 "학습된 규칙" 에서 제외하고 별도 표시.
122
+ // 이전에는 디렉터리 파일 수만 세어 이미 제거된 rule 도 count 되어 판매 관점
123
+ // "살아있는 규칙" 수치가 부풀려짐. 실제 구매자 가치는 active + suppressed.
124
+ const ruleFiles = fs.readdirSync(ME_RULES).filter(isKnowledgeFile);
125
+ let active = 0, suppressed = 0, removed = 0;
126
+ for (const f of ruleFiles) {
127
+ try {
128
+ const d = JSON.parse(fs.readFileSync(path.join(ME_RULES, f), 'utf-8'));
129
+ if (d.status === 'active')
130
+ active++;
131
+ else if (d.status === 'suppressed')
132
+ suppressed++;
133
+ else if (d.status === 'removed' || d.status === 'superseded')
134
+ removed++;
135
+ }
136
+ catch { /* skip */ }
137
+ }
138
+ const live = active + suppressed;
139
+ const removedTag = removed > 0 ? ` (${removed} removed/superseded)` : '';
140
+ console.log(` Personal rules: ${live} [active:${active} suppressed:${suppressed}]${removedTag}`);
99
141
  }
100
142
  console.log();
101
143
  console.log(' [Log Locations]');
@@ -323,6 +365,15 @@ export async function runDoctor(opts = {}) {
323
365
  const report = pruneState({ dryRun: false });
324
366
  const mb = (report.bytesFreed / 1024 / 1024).toFixed(2);
325
367
  console.log(` → Pruned ${report.pruned}/${report.scanned} files (${mb} MB freed, >${report.retentionDays}d old)`);
368
+ // ADR-002 T4 — 90d 미주입 rule retire. pruneState 와 함께 "하루 한번 정돈" 의미 공유.
369
+ try {
370
+ const { runDailyT4Decay } = await import('./state-gc.js');
371
+ const t4 = await runDailyT4Decay({ dryRun: false });
372
+ if (t4.retired > 0) {
373
+ console.log(` → Retired ${t4.retired} rule(s) (T4 time-decay): ${t4.sample.join(', ')}`);
374
+ }
375
+ }
376
+ catch { /* fail-open */ }
326
377
  }
327
378
  console.log();
328
379
  // 현재 디렉토리 git 정보
@@ -336,4 +387,28 @@ export async function runDoctor(opts = {}) {
336
387
  console.log(' git remote: (none)');
337
388
  }
338
389
  console.log();
390
+ // [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
391
+ console.log(' [Summary]');
392
+ if (failedChecks.length === 0) {
393
+ console.log(' ✓ All diagnostics passed. Forgen is ready.');
394
+ }
395
+ else {
396
+ console.log(` ✗ ${failedChecks.length} check(s) failed:\n`);
397
+ const bySection = new Map();
398
+ for (const f of failedChecks) {
399
+ if (!bySection.has(f.section))
400
+ bySection.set(f.section, []);
401
+ bySection.get(f.section).push(f);
402
+ }
403
+ for (const [sec, items] of bySection) {
404
+ console.log(` [${sec}]`);
405
+ for (const item of items) {
406
+ console.log(` • ${item.label}`);
407
+ if (item.hint)
408
+ console.log(` → ${item.hint}`);
409
+ }
410
+ }
411
+ console.log('\n Run `forgen doctor` again after applying the fixes above.');
412
+ }
413
+ console.log();
339
414
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Forgen v0.4.1 — Extraction Notice (H2)
3
+ *
4
+ * `~/.forgen/state/last-auto-compound.json` 에 기록된 이전 세션의 추출 결과를
5
+ * Stop hook 에서 1회 surface. noticeShown 플래그로 한번 보여주면 다시 안뜸.
6
+ *
7
+ * 목적: v0.4.0 에서 auto-compound 가 8,000+ 번 돌았는데 사용자는 0건 노출. 추출이
8
+ * 실제로 일어났는지 사용자가 확인할 수 없었다. H2 는 "세션 종료 시 N개 패턴
9
+ * 학습됨" 1줄을 Stop hook UI (systemMessage) 로 밀어넣는다.
10
+ */
11
+ /**
12
+ * Stop hook approve 경로에서 호출. 보여줄 알림이 있으면 1줄 문자열 반환하고
13
+ * noticeShown=true 로 파일 업데이트 (한 번만 surface). 없으면 null.
14
+ *
15
+ * 신선도 컷오프: completedAt 이 30분 이상 지나면 stale 로 간주하고 surface 안함.
16
+ * 이미 다른 세션에서 본 알림이 튀어나오는 걸 방지.
17
+ */
18
+ export declare function takeLastExtractionNotice(nowMs?: number): string | null;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Forgen v0.4.1 — Extraction Notice (H2)
3
+ *
4
+ * `~/.forgen/state/last-auto-compound.json` 에 기록된 이전 세션의 추출 결과를
5
+ * Stop hook 에서 1회 surface. noticeShown 플래그로 한번 보여주면 다시 안뜸.
6
+ *
7
+ * 목적: v0.4.0 에서 auto-compound 가 8,000+ 번 돌았는데 사용자는 0건 노출. 추출이
8
+ * 실제로 일어났는지 사용자가 확인할 수 없었다. H2 는 "세션 종료 시 N개 패턴
9
+ * 학습됨" 1줄을 Stop hook UI (systemMessage) 로 밀어넣는다.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { STATE_DIR } from './paths.js';
14
+ const LAST_AUTO_COMPOUND_PATH = path.join(STATE_DIR, 'last-auto-compound.json');
15
+ /** 정상 실행이면 건너뛰기 좋게 fail-open. */
16
+ function readRecord() {
17
+ try {
18
+ if (!fs.existsSync(LAST_AUTO_COMPOUND_PATH))
19
+ return null;
20
+ return JSON.parse(fs.readFileSync(LAST_AUTO_COMPOUND_PATH, 'utf-8'));
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /**
27
+ * Stop hook approve 경로에서 호출. 보여줄 알림이 있으면 1줄 문자열 반환하고
28
+ * noticeShown=true 로 파일 업데이트 (한 번만 surface). 없으면 null.
29
+ *
30
+ * 신선도 컷오프: completedAt 이 30분 이상 지나면 stale 로 간주하고 surface 안함.
31
+ * 이미 다른 세션에서 본 알림이 튀어나오는 걸 방지.
32
+ */
33
+ export function takeLastExtractionNotice(nowMs = Date.now()) {
34
+ const record = readRecord();
35
+ if (!record || record.noticeShown)
36
+ return null;
37
+ const completed = Date.parse(record.completedAt);
38
+ if (!Number.isFinite(completed))
39
+ return null;
40
+ const ageMs = nowMs - completed;
41
+ if (ageMs > 30 * 60 * 1000)
42
+ return null; // stale
43
+ const extracted = record.extractedSolutions ?? 0;
44
+ const promoted = record.promotedRules ?? 0;
45
+ if (extracted === 0 && promoted === 0) {
46
+ // 아무것도 학습되지 않았으면 노이즈. 알림을 소비한 상태로만 마킹.
47
+ try {
48
+ fs.writeFileSync(LAST_AUTO_COMPOUND_PATH, JSON.stringify({ ...record, noticeShown: true }));
49
+ }
50
+ catch { /* fail-open */ }
51
+ return null;
52
+ }
53
+ // 마킹 — race 는 있으나 double-notice 가 치명적이지 않음 (fail-open).
54
+ try {
55
+ fs.writeFileSync(LAST_AUTO_COMPOUND_PATH, JSON.stringify({ ...record, noticeShown: true }));
56
+ }
57
+ catch { /* fail-open */ }
58
+ const parts = [];
59
+ if (extracted > 0)
60
+ parts.push(`${extracted}개 패턴 추출`);
61
+ if (promoted > 0)
62
+ parts.push(`${promoted}개 규칙 승격`);
63
+ return `[Forgen] 🧠 세션 학습 완료 — ${parts.join(', ')}`;
64
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Forgen v0.4.1 — `forgen init` CLI
3
+ *
4
+ * 빈 FORGEN_HOME (또는 기존에 starter 미설치 홈) 에 starter-pack 솔루션을
5
+ * 프로비저닝. npm install-g 시의 postinstall 이 하던 starter 배포 로직을 런타임
6
+ * CLI 로 노출해 다음 시나리오 지원:
7
+ * - `FORGEN_HOME=/tmp/fresh forgen init` — 격리 테스트 환경
8
+ * - CI pipeline 신규 컨테이너 프로비저닝
9
+ * - 사용자가 실수로 me/solutions 전부 삭제한 뒤 복구
10
+ *
11
+ * 보수적 정책: me/solutions 에 **≥5개 파일**이 이미 있으면 건너뜀 (사용자
12
+ * 실 축적물 보호). `--force` 플래그로 우회 가능. postinstall 의 installStarterPack
13
+ * 과 동일 규칙.
14
+ */
15
+ export interface InitResult {
16
+ solutionsInstalled: number;
17
+ solutionsSkippedExisting: number;
18
+ solutionsDir: string;
19
+ starterDir: string | null;
20
+ skipped: boolean;
21
+ skipReason?: string;
22
+ }
23
+ export declare function initializeForgenHome(options?: {
24
+ force?: boolean;
25
+ }): InitResult;
26
+ export declare function handleInit(args: string[]): Promise<void>;