@wooojin/forgen 0.4.1 → 0.4.4

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 (151) hide show
  1. package/.claude-plugin/plugin.json +5 -5
  2. package/CHANGELOG.md +267 -15
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.ja.md +17 -9
  5. package/README.ko.md +34 -12
  6. package/README.md +65 -12
  7. package/README.zh.md +17 -9
  8. package/assets/README.md +86 -0
  9. package/assets/architecture.svg +100 -0
  10. package/assets/banner.png +0 -0
  11. package/assets/banner.svg +53 -0
  12. package/{commands → assets/claude/commands}/calibrate.md +4 -3
  13. package/{commands → assets/claude/commands}/retro.md +2 -2
  14. package/assets/demo/01-install.gif +0 -0
  15. package/assets/demo/01-install.tape +54 -0
  16. package/assets/demo/02-compound-learning.gif +0 -0
  17. package/assets/demo/02-compound-learning.tape +50 -0
  18. package/assets/demo/03-forge-personalization.gif +0 -0
  19. package/assets/demo/03-forge-personalization.tape +64 -0
  20. package/assets/demo/before-after.gif +0 -0
  21. package/assets/demo/before-after.tape +98 -0
  22. package/assets/demo-preview.svg +96 -0
  23. package/assets/icon.png +0 -0
  24. package/{hooks → assets/shared}/hook-registry.json +2 -1
  25. package/dist/checks/_shared/text-sanitizer.d.ts +21 -0
  26. package/dist/checks/_shared/text-sanitizer.js +60 -0
  27. package/dist/checks/dangerous-response-pattern.d.ts +32 -0
  28. package/dist/checks/dangerous-response-pattern.js +65 -0
  29. package/dist/checks/fact-vs-agreement.js +25 -1
  30. package/dist/cli.js +78 -6
  31. package/dist/core/auto-compound-runner.js +90 -39
  32. package/dist/core/behavior-classifier.d.ts +28 -0
  33. package/dist/core/behavior-classifier.js +46 -0
  34. package/dist/core/dashboard.d.ts +7 -0
  35. package/dist/core/dashboard.js +32 -0
  36. package/dist/core/doctor.js +92 -0
  37. package/dist/core/git-stats.d.ts +36 -0
  38. package/dist/core/git-stats.js +79 -0
  39. package/dist/core/harness.d.ts +1 -1
  40. package/dist/core/harness.js +27 -20
  41. package/dist/core/host-detect.d.ts +42 -0
  42. package/dist/core/host-detect.js +68 -0
  43. package/dist/core/installer.js +2 -2
  44. package/dist/core/migrate-cli.d.ts +1 -0
  45. package/dist/core/migrate-cli.js +19 -0
  46. package/dist/core/migrate-evidence-host.d.ts +36 -0
  47. package/dist/core/migrate-evidence-host.js +49 -0
  48. package/dist/core/settings-injector.js +4 -2
  49. package/dist/core/spawn.d.ts +1 -1
  50. package/dist/core/spawn.js +4 -11
  51. package/dist/core/stats-cli.js +12 -0
  52. package/dist/core/trust-layer-intent.d.ts +35 -0
  53. package/dist/core/trust-layer-intent.js +30 -0
  54. package/dist/core/types.d.ts +1 -1
  55. package/dist/engine/compound-extractor.js +7 -9
  56. package/dist/engine/learn-cli.js +4 -2
  57. package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
  58. package/dist/engine/lifecycle/bypass-detector.js +57 -5
  59. package/dist/fgx.js +2 -1
  60. package/dist/forge/evidence-processor.js +12 -0
  61. package/dist/forge/onboarding.d.ts +3 -2
  62. package/dist/forge/onboarding.js +3 -2
  63. package/dist/hooks/db-guard.js +3 -3
  64. package/dist/hooks/forge-loop-progress.d.ts +9 -0
  65. package/dist/hooks/forge-loop-progress.js +38 -0
  66. package/dist/hooks/hook-registry.js +1 -1
  67. package/dist/hooks/hooks-generator.d.ts +15 -1
  68. package/dist/hooks/hooks-generator.js +18 -16
  69. package/dist/hooks/keyword-detector.js +1 -1
  70. package/dist/hooks/post-tool-use.d.ts +1 -1
  71. package/dist/hooks/post-tool-use.js +13 -4
  72. package/dist/hooks/pre-compact.js +1 -1
  73. package/dist/hooks/pre-tool-use.js +4 -4
  74. package/dist/hooks/rate-limiter.js +2 -2
  75. package/dist/hooks/session-recovery.js +11 -0
  76. package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
  77. package/dist/hooks/shared/blocking-allowlist.js +38 -0
  78. package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
  79. package/dist/hooks/shared/forge-loop-state.js +116 -0
  80. package/dist/hooks/shared/hook-response.d.ts +18 -0
  81. package/dist/hooks/shared/hook-response.js +31 -0
  82. package/dist/hooks/skill-injector.js +1 -1
  83. package/dist/hooks/stop-guard.js +57 -25
  84. package/dist/host/capabilities-claude.d.ts +8 -0
  85. package/dist/host/capabilities-claude.js +46 -0
  86. package/dist/host/capabilities-codex.d.ts +11 -0
  87. package/dist/host/capabilities-codex.js +50 -0
  88. package/dist/host/capabilities-registry.d.ts +11 -0
  89. package/dist/host/capabilities-registry.js +30 -0
  90. package/dist/host/codex-adapter.d.ts +8 -5
  91. package/dist/host/codex-adapter.js +10 -82
  92. package/dist/host/codex-output-parser.d.ts +39 -0
  93. package/dist/host/codex-output-parser.js +75 -0
  94. package/dist/host/exec-host.d.ts +54 -0
  95. package/dist/host/exec-host.js +92 -0
  96. package/dist/host/host-runtime.d.ts +37 -0
  97. package/dist/host/host-runtime.js +51 -0
  98. package/dist/host/install-claude.d.ts +35 -0
  99. package/dist/host/install-claude.js +238 -0
  100. package/dist/host/install-codex.d.ts +44 -0
  101. package/dist/host/install-codex.js +276 -0
  102. package/dist/host/install-orchestrator.d.ts +34 -0
  103. package/dist/host/install-orchestrator.js +126 -0
  104. package/dist/host/invoke-agent.d.ts +27 -0
  105. package/dist/host/invoke-agent.js +115 -0
  106. package/dist/host/parity-harness.d.ts +62 -0
  107. package/dist/host/parity-harness.js +283 -0
  108. package/dist/host/projection.d.ts +35 -0
  109. package/dist/host/projection.js +126 -0
  110. package/dist/mcp/server.js +11 -0
  111. package/dist/mcp/tools.js +51 -0
  112. package/dist/renderer/rule-renderer.d.ts +1 -1
  113. package/dist/renderer/rule-renderer.js +73 -1
  114. package/dist/services/session.d.ts +6 -3
  115. package/dist/services/session.js +33 -4
  116. package/dist/store/compound-usage-store.d.ts +28 -0
  117. package/dist/store/compound-usage-store.js +59 -0
  118. package/dist/store/evidence-store.d.ts +1 -0
  119. package/dist/store/evidence-store.js +34 -3
  120. package/dist/store/host-mismatch.d.ts +42 -0
  121. package/dist/store/host-mismatch.js +65 -0
  122. package/dist/store/profile-store.d.ts +29 -0
  123. package/dist/store/profile-store.js +53 -0
  124. package/dist/store/types.d.ts +13 -0
  125. package/hooks/hooks.json +6 -1
  126. package/package.json +6 -4
  127. package/plugin.json +4 -4
  128. package/scripts/postinstall.js +100 -25
  129. package/skills/calibrate/SKILL.md +4 -3
  130. package/skills/retro/SKILL.md +2 -2
  131. /package/{agents → assets/claude/agents}/analyst.md +0 -0
  132. /package/{agents → assets/claude/agents}/architect.md +0 -0
  133. /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
  134. /package/{agents → assets/claude/agents}/critic.md +0 -0
  135. /package/{agents → assets/claude/agents}/debugger.md +0 -0
  136. /package/{agents → assets/claude/agents}/designer.md +0 -0
  137. /package/{agents → assets/claude/agents}/executor.md +0 -0
  138. /package/{agents → assets/claude/agents}/explore.md +0 -0
  139. /package/{agents → assets/claude/agents}/git-master.md +0 -0
  140. /package/{agents → assets/claude/agents}/planner.md +0 -0
  141. /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
  142. /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
  143. /package/{agents → assets/claude/agents}/verifier.md +0 -0
  144. /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
  145. /package/{commands → assets/claude/commands}/code-review.md +0 -0
  146. /package/{commands → assets/claude/commands}/compound.md +0 -0
  147. /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
  148. /package/{commands → assets/claude/commands}/docker.md +0 -0
  149. /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
  150. /package/{commands → assets/claude/commands}/learn.md +0 -0
  151. /package/{commands → assets/claude/commands}/ship.md +0 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Host detection — feat/codex-support Phase 1
3
+ *
4
+ * `forgen install` interactive 의 prerequisite — 사용자 환경에 어떤 host (Claude/Codex)
5
+ * 가 가용한지 탐지. spec §10 Phase 1 + interview R3.
6
+ *
7
+ * 탐지 신호 (각 host 별):
8
+ * - binary 가 PATH 에 있음 (`which claude` / `which codex`)
9
+ * - host 디렉토리 존재 (~/.claude/ / ~/.codex/)
10
+ * - (Codex 만) `~/.codex/auth.json` 존재 (로그인 흔적)
11
+ *
12
+ * detect 결과는 *추론* 만. install 강제 안 함.
13
+ */
14
+ import type { HostId } from './trust-layer-intent.js';
15
+ export interface HostAvailability {
16
+ readonly host: HostId;
17
+ /** binary 가 PATH 에 있음. */
18
+ readonly binaryFound: boolean;
19
+ /** binary 절대경로 (없으면 null). */
20
+ readonly binaryPath: string | null;
21
+ /** host home 디렉토리 존재 (~/.claude/ 또는 ~/.codex/). */
22
+ readonly homeExists: boolean;
23
+ /** host home 절대경로. */
24
+ readonly homePath: string;
25
+ /** Codex 의 경우 auth.json 존재 (로그인 흔적). Claude 는 항상 null. */
26
+ readonly authPresent: boolean | null;
27
+ /**
28
+ * 종합 판단 — *install 후보로 적합한가*.
29
+ * - binaryFound 또는 homeExists 중 하나 이상이면 true.
30
+ * - 둘 다 없으면 false (사용자가 host 를 안 쓸 가능성 높음).
31
+ */
32
+ readonly available: boolean;
33
+ }
34
+ export interface HostDetectionResult {
35
+ readonly claude: HostAvailability;
36
+ readonly codex: HostAvailability;
37
+ /** 둘 다 사용 가능. */
38
+ readonly bothAvailable: boolean;
39
+ /** 하나도 사용 가능하지 않음 (warn). */
40
+ readonly noneAvailable: boolean;
41
+ }
42
+ export declare function detectAvailableHosts(): HostDetectionResult;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Host detection — feat/codex-support Phase 1
3
+ *
4
+ * `forgen install` interactive 의 prerequisite — 사용자 환경에 어떤 host (Claude/Codex)
5
+ * 가 가용한지 탐지. spec §10 Phase 1 + interview R3.
6
+ *
7
+ * 탐지 신호 (각 host 별):
8
+ * - binary 가 PATH 에 있음 (`which claude` / `which codex`)
9
+ * - host 디렉토리 존재 (~/.claude/ / ~/.codex/)
10
+ * - (Codex 만) `~/.codex/auth.json` 존재 (로그인 흔적)
11
+ *
12
+ * detect 결과는 *추론* 만. install 강제 안 함.
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as os from 'node:os';
16
+ import * as path from 'node:path';
17
+ import { execFileSync } from 'node:child_process';
18
+ function which(binary) {
19
+ try {
20
+ const out = execFileSync('which', [binary], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
21
+ const trimmed = out.trim();
22
+ return trimmed.length > 0 ? trimmed : null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function detectClaude() {
29
+ const binaryPath = which('claude');
30
+ const homePath = path.join(os.homedir(), '.claude');
31
+ const homeExists = fs.existsSync(homePath);
32
+ const binaryFound = binaryPath !== null;
33
+ return {
34
+ host: 'claude',
35
+ binaryFound,
36
+ binaryPath,
37
+ homeExists,
38
+ homePath,
39
+ authPresent: null, // Claude 는 별도 auth.json 패턴이 없음 (subscription 통합)
40
+ available: binaryFound || homeExists,
41
+ };
42
+ }
43
+ function detectCodex() {
44
+ const binaryPath = which('codex');
45
+ const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
46
+ const homeExists = fs.existsSync(codexHome);
47
+ const binaryFound = binaryPath !== null;
48
+ const authPresent = fs.existsSync(path.join(codexHome, 'auth.json'));
49
+ return {
50
+ host: 'codex',
51
+ binaryFound,
52
+ binaryPath,
53
+ homeExists,
54
+ homePath: codexHome,
55
+ authPresent,
56
+ available: binaryFound || homeExists,
57
+ };
58
+ }
59
+ export function detectAvailableHosts() {
60
+ const claude = detectClaude();
61
+ const codex = detectCodex();
62
+ return {
63
+ claude,
64
+ codex,
65
+ bothAvailable: claude.available && codex.available,
66
+ noneAvailable: !claude.available && !codex.available,
67
+ };
68
+ }
@@ -111,7 +111,7 @@ export function installAgents(cwd, pkgRoot) {
111
111
  const targetDir = path.join(cwd, '.claude', 'agents');
112
112
  fs.mkdirSync(targetDir, { recursive: true });
113
113
  const hashes = loadAgentHashes();
114
- const sourceDir = path.join(pkgRoot, 'agents');
114
+ const sourceDir = path.join(pkgRoot, 'assets', 'claude', 'agents');
115
115
  try {
116
116
  installAgentsFromDir(sourceDir, targetDir, 'ch-', hashes);
117
117
  cleanupStaleAgents(sourceDir, targetDir, 'ch-', hashes);
@@ -159,7 +159,7 @@ function cleanupStaleCommands(commandsDir, validFiles) {
159
159
  }
160
160
  /** 스킬을 Claude Code 슬래시 명령으로 설치 (패키지 내장만) */
161
161
  export function installSlashCommands(_cwd, pkgRoot) {
162
- let skillsDir = path.join(pkgRoot, 'commands');
162
+ let skillsDir = path.join(pkgRoot, 'assets', 'claude', 'commands');
163
163
  if (!fs.existsSync(skillsDir)) {
164
164
  skillsDir = path.join(pkgRoot, 'skills');
165
165
  }
@@ -6,5 +6,6 @@
6
6
  * - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
7
7
  * 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
8
8
  * 경우 영구 재기록이 필요.
9
+ * - evidence-host: ~/.forgen/me/behavior/*.json 에 host 필드가 없는 파일 백필.
9
10
  */
10
11
  export declare function handleMigrate(args: string[]): Promise<void>;
@@ -6,14 +6,19 @@
6
6
  * - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
7
7
  * 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
8
8
  * 경우 영구 재기록이 필요.
9
+ * - evidence-host: ~/.forgen/me/behavior/*.json 에 host 필드가 없는 파일 백필.
9
10
  */
10
11
  import { migrateImplicitFeedbackLog } from '../store/implicit-feedback-store.js';
12
+ import { migrateEvidenceHost } from './migrate-evidence-host.js';
11
13
  const HELP = `
12
14
  forgen migrate — one-shot schema migrations
13
15
 
14
16
  Usage:
15
17
  forgen migrate implicit-feedback category 필드가 없는 레거시 엔트리 백필 + 재기록
16
18
  forgen migrate all (현재는 implicit-feedback 과 동일)
19
+ forgen migrate evidence-host behavior/*.json 에 host 필드 백필
20
+ --dry-run 디스크 미수정, 카운트만 출력
21
+ --default-host <claude|codex> host 기본값 (default: claude)
17
22
  forgen migrate --help 이 도움말
18
23
  `;
19
24
  export async function handleMigrate(args) {
@@ -28,6 +33,20 @@ export async function handleMigrate(args) {
28
33
  console.log(`[forgen migrate] 백필 ${migrated}건, 드롭 ${dropped}건 — 재기록 완료.`);
29
34
  return;
30
35
  }
36
+ if (sub === 'evidence-host') {
37
+ const dryRun = args.includes('--dry-run');
38
+ const hostFlagIdx = args.indexOf('--default-host');
39
+ const rawHost = hostFlagIdx !== -1 ? args[hostFlagIdx + 1] : 'claude';
40
+ if (rawHost !== 'claude' && rawHost !== 'codex') {
41
+ console.error(`[forgen migrate] --default-host 는 'claude' 또는 'codex' 여야 합니다. 받은 값: ${rawHost}`);
42
+ process.exit(1);
43
+ }
44
+ const defaultHost = rawHost;
45
+ const result = migrateEvidenceHost({ defaultHost, dryRun });
46
+ const label = dryRun ? ' (dry-run)' : '';
47
+ console.log(`[forgen] migrated: ${result.migrated} (skipped: ${result.skipped}, total: ${result.total})${label}`);
48
+ return;
49
+ }
31
50
  console.error(`[forgen migrate] unknown target: ${sub}`);
32
51
  console.error(HELP);
33
52
  process.exit(1);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Forgen — Evidence Host Backfill Migration
3
+ *
4
+ * spec §10 우선순위 5 + §4.2:
5
+ * 사용자가 명시적으로 디스크의 evidence 파일에 host 필드를 박제하고 싶을 때 사용.
6
+ * (마이그레이션 박제, audit 목적)
7
+ *
8
+ * loadEvidence 의 자동 backfill (evidence-store.ts::backfillHost) 과는 독립.
9
+ * 이 모듈은 디스크 파일을 직접 수정하는 명시적 경로.
10
+ */
11
+ export interface MigrateEvidenceHostOptions {
12
+ /** backfill 할 host 값 (host 필드 없는 파일에만 적용) */
13
+ defaultHost: 'claude' | 'codex';
14
+ /**
15
+ * true 이면 디스크를 수정하지 않고 카운트만 반환.
16
+ * @default false
17
+ */
18
+ dryRun?: boolean;
19
+ }
20
+ export interface MigrateEvidenceHostResult {
21
+ /** host 필드를 새로 추가한 파일 수 */
22
+ migrated: number;
23
+ /** 처리 대상 전체 .json 파일 수 */
24
+ total: number;
25
+ /** 이미 host 필드가 있어 건너뛴 파일 수 */
26
+ skipped: number;
27
+ }
28
+ /**
29
+ * `~/.forgen/me/behavior/*.json` 을 순회하여 host 필드가 없는 파일에
30
+ * `defaultHost` 를 추가한다.
31
+ *
32
+ * - dryRun=true 이면 디스크 미수정, 카운트만 반환.
33
+ * - 이미 host 필드가 있는 파일은 건너뜀 (idempotent).
34
+ * - 파싱 실패 / host 필드가 아닌 값인 파일도 건너뜀 (안전).
35
+ */
36
+ export declare function migrateEvidenceHost(options: MigrateEvidenceHostOptions): MigrateEvidenceHostResult;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Forgen — Evidence Host Backfill Migration
3
+ *
4
+ * spec §10 우선순위 5 + §4.2:
5
+ * 사용자가 명시적으로 디스크의 evidence 파일에 host 필드를 박제하고 싶을 때 사용.
6
+ * (마이그레이션 박제, audit 목적)
7
+ *
8
+ * loadEvidence 의 자동 backfill (evidence-store.ts::backfillHost) 과는 독립.
9
+ * 이 모듈은 디스크 파일을 직접 수정하는 명시적 경로.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { ME_BEHAVIOR } from './paths.js';
14
+ import { atomicWriteJSON, safeReadJSON } from '../hooks/shared/atomic-write.js';
15
+ /**
16
+ * `~/.forgen/me/behavior/*.json` 을 순회하여 host 필드가 없는 파일에
17
+ * `defaultHost` 를 추가한다.
18
+ *
19
+ * - dryRun=true 이면 디스크 미수정, 카운트만 반환.
20
+ * - 이미 host 필드가 있는 파일은 건너뜀 (idempotent).
21
+ * - 파싱 실패 / host 필드가 아닌 값인 파일도 건너뜀 (안전).
22
+ */
23
+ export function migrateEvidenceHost(options) {
24
+ const { defaultHost, dryRun = false } = options;
25
+ if (!fs.existsSync(ME_BEHAVIOR)) {
26
+ return { migrated: 0, total: 0, skipped: 0 };
27
+ }
28
+ const files = fs.readdirSync(ME_BEHAVIOR).filter((f) => f.endsWith('.json'));
29
+ let migrated = 0;
30
+ let skipped = 0;
31
+ for (const file of files) {
32
+ const filePath = path.join(ME_BEHAVIOR, file);
33
+ const data = safeReadJSON(filePath, null);
34
+ if (data === null || typeof data !== 'object') {
35
+ skipped++;
36
+ continue;
37
+ }
38
+ const host = data['host'];
39
+ if (host === 'claude' || host === 'codex') {
40
+ skipped++;
41
+ continue;
42
+ }
43
+ if (!dryRun) {
44
+ atomicWriteJSON(filePath, { ...data, host: defaultHost }, { pretty: true });
45
+ }
46
+ migrated++;
47
+ }
48
+ return { migrated, total: files.length, skipped };
49
+ }
@@ -7,6 +7,7 @@
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { generateHooksJson } from '../hooks/hooks-generator.js';
10
+ import { getHostRuntime } from '../host/host-runtime.js';
10
11
  import { ConfigError } from './errors.js';
11
12
  import { createLogger } from './logger.js';
12
13
  import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, readSettingsSafely, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
@@ -74,7 +75,8 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
74
75
  hooksConfig[event] = filtered;
75
76
  }
76
77
  try {
77
- if (runtime === 'codex') {
78
+ const host = getHostRuntime(runtime);
79
+ if (host.hookInjectionStrategy === 'generate') {
78
80
  const generated = generateHooksJson({ cwd, runtime, pluginRoot: path.join(pkgRoot, 'dist') });
79
81
  for (const [event, handlers] of Object.entries(generated.hooks)) {
80
82
  if (!hooksConfig[event])
@@ -83,7 +85,7 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
83
85
  }
84
86
  }
85
87
  else {
86
- // Read hooks.json and inject, replacing ${CLAUDE_PLUGIN_ROOT}
88
+ // 'pre-baked-file': pkgRoot/hooks/hooks.json 읽고 ${CLAUDE_PLUGIN_ROOT} 치환
87
89
  const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
88
90
  if (fs.existsSync(hooksJsonPath)) {
89
91
  const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
@@ -1,5 +1,5 @@
1
1
  import type { V1HarnessContext } from './harness.js';
2
- import { type RuntimeHost } from './types.js';
2
+ import type { RuntimeHost } from './types.js';
3
3
  /** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
4
4
  export declare function spawnClaude(args: string[], context: V1HarnessContext, runtime?: RuntimeHost): Promise<number>;
5
5
  /**
@@ -7,13 +7,11 @@ import { buildEnv } from './config-injector.js';
7
7
  import { loadGlobalConfig } from './global-config.js';
8
8
  import { createLogger } from './logger.js';
9
9
  import { STATE_DIR } from './paths.js';
10
+ import { getHostRuntime } from '../host/host-runtime.js';
10
11
  const log = createLogger('spawn');
11
- /** claude CLI 경로 탐색 */
12
- function findClaude() {
13
- return 'claude';
14
- }
12
+ /** Phase 2: host-runtime 어댑터 위임. */
15
13
  function findRuntimeLauncher(runtime) {
16
- return runtime === 'codex' ? 'codex' : findClaude();
14
+ return getHostRuntime(runtime).launcher;
17
15
  }
18
16
  function transcriptProjectDir(cwd) {
19
17
  // Claude Code는 cwd의 /를 -로 치환하고 선행 -를 유지
@@ -161,12 +159,7 @@ export async function spawnClaude(args, context, runtime = 'claude') {
161
159
  });
162
160
  child.on('error', (err) => {
163
161
  if (err.code === 'ENOENT') {
164
- if (runtime === 'codex') {
165
- reject(new Error('Codex is not installed.'));
166
- }
167
- else {
168
- reject(new Error('Claude Code is not installed. npm install -g @anthropic-ai/claude-code'));
169
- }
162
+ reject(new Error(getHostRuntime(runtime).missingInstallMessage));
170
163
  }
171
164
  else {
172
165
  reject(err);
@@ -10,6 +10,7 @@ import * as path from 'node:path';
10
10
  import { loadAllRules } from '../store/rule-store.js';
11
11
  import { loadAllEvidence } from '../store/evidence-store.js';
12
12
  import { STATE_DIR, ME_DIR } from './paths.js';
13
+ import { computeFixFeatRatio, formatFixRatio } from './git-stats.js';
13
14
  // v0.4.1 격리 fix: 이전에는 os.homedir() 직접 사용해서 FORGEN_HOME env 로
14
15
  // 홈 격리해도 이 파일의 경로는 여전히 실 홈 가리켰음. paths.ts 상수 import.
15
16
  const ENFORCEMENT_DIR = path.join(STATE_DIR, 'enforcement');
@@ -244,6 +245,17 @@ export function renderStats(s) {
244
245
  lines.push(` Last reclass ${s.philosophy.lastReclassification ?? 'never'}`);
245
246
  lines.push('');
246
247
  }
248
+ // P4 셀프 가드 — 최근 30커밋 fix:feat 비율로 회귀 패턴 자가 노출.
249
+ // 30% 초과 시 "이거 고치면 저거 버그난다" 패턴 의심 → forgen doctor 가 경고.
250
+ try {
251
+ const ratio = computeFixFeatRatio();
252
+ if (ratio.available) {
253
+ lines.push(' Repo health (last 30 commits)');
254
+ lines.push(` ${formatFixRatio(ratio)}`);
255
+ lines.push('');
256
+ }
257
+ }
258
+ catch { /* fail-open: git 없거나 비-repo 환경 */ }
247
259
  lines.push(` Last extraction: ${s.lastExtraction}`);
248
260
  lines.push('');
249
261
  return lines.join('\n');
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Trust Layer Intent — Multi-Host Core Design §9.0 산출물 #1
3
+ *
4
+ * forgen 이 host 위에서 보장하는 행동의 enum. spec §9.0 의 7 의도 매트릭스와 1:1.
5
+ * 각 host adapter 는 이 enum 의 모든 항목에 대해 CapabilityDeclaration 을 선언해야 하며,
6
+ * 미선언은 컴파일 타임(`Record<TrustLayerIntent, _>`) + 런타임(`assertCapabilitiesComplete`) 양쪽에서 fail.
7
+ *
8
+ * 1원칙: Claude semantics 가 reference. 본 enum 의 의미는 Claude Hook schema 의 행동을 그대로 사용한다.
9
+ */
10
+ export declare const TRUST_LAYER_INTENTS: readonly ["block-completion", "block-tool-use", "inject-context", "observe-only", "secret-filter", "forge-loop-state-inject", "self-evidence-record"];
11
+ export type TrustLayerIntent = (typeof TRUST_LAYER_INTENTS)[number];
12
+ export type CapabilityStatus = 'supported' | 'partial' | 'unsupported';
13
+ export interface CapabilityDeclaration {
14
+ readonly status: CapabilityStatus;
15
+ /** host 표면이 이 의도를 표현하는 hook/필드 (예: "Stop + decision:'block' + reason"). */
16
+ readonly expression: string;
17
+ /** partial/unsupported 시 등가성 보존을 위한 mitigation 핸들. supported 면 undefined. */
18
+ readonly mitigation?: string;
19
+ /** source-of-truth (spec 또는 외부 docs/source 인용). */
20
+ readonly source?: string;
21
+ }
22
+ export type HostId = 'claude' | 'codex';
23
+ export interface HostCapabilities {
24
+ readonly hostId: HostId;
25
+ /**
26
+ * 모든 TrustLayerIntent 에 대한 선언. `Record<TrustLayerIntent, _>` 타입이
27
+ * 컴파일 타임에 누락을 차단한다.
28
+ */
29
+ readonly intents: Record<TrustLayerIntent, CapabilityDeclaration>;
30
+ }
31
+ /**
32
+ * 런타임 assertion — host adapter 가 새 의도 추가를 누락한 경우 fail.
33
+ * 컴파일 타임 가드를 우회하는 동적 생성 코드를 위한 안전망.
34
+ */
35
+ export declare function assertCapabilitiesComplete(caps: HostCapabilities): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Trust Layer Intent — Multi-Host Core Design §9.0 산출물 #1
3
+ *
4
+ * forgen 이 host 위에서 보장하는 행동의 enum. spec §9.0 의 7 의도 매트릭스와 1:1.
5
+ * 각 host adapter 는 이 enum 의 모든 항목에 대해 CapabilityDeclaration 을 선언해야 하며,
6
+ * 미선언은 컴파일 타임(`Record<TrustLayerIntent, _>`) + 런타임(`assertCapabilitiesComplete`) 양쪽에서 fail.
7
+ *
8
+ * 1원칙: Claude semantics 가 reference. 본 enum 의 의미는 Claude Hook schema 의 행동을 그대로 사용한다.
9
+ */
10
+ export const TRUST_LAYER_INTENTS = [
11
+ 'block-completion',
12
+ 'block-tool-use',
13
+ 'inject-context',
14
+ 'observe-only',
15
+ 'secret-filter',
16
+ 'forge-loop-state-inject',
17
+ 'self-evidence-record',
18
+ ];
19
+ /**
20
+ * 런타임 assertion — host adapter 가 새 의도 추가를 누락한 경우 fail.
21
+ * 컴파일 타임 가드를 우회하는 동적 생성 코드를 위한 안전망.
22
+ */
23
+ export function assertCapabilitiesComplete(caps) {
24
+ const declared = new Set(Object.keys(caps.intents));
25
+ const missing = TRUST_LAYER_INTENTS.filter((i) => !declared.has(i));
26
+ if (missing.length > 0) {
27
+ throw new Error(`HostCapabilities for "${caps.hostId}" missing intents: ${missing.join(', ')}. ` +
28
+ `All TrustLayerIntent values must be declared (spec §9.0).`);
29
+ }
30
+ }
@@ -112,7 +112,7 @@ export type RuntimeHost = 'claude' | 'codex';
112
112
  export interface LaunchContext {
113
113
  runtime: RuntimeHost;
114
114
  args: string[];
115
- runtimeSource: 'flag' | 'env' | 'default';
115
+ runtimeSource: 'flag' | 'env' | 'profile' | 'default';
116
116
  }
117
117
  /** 훅 입력 이벤트 스키마 (버전 간 상위 호환용 최소 스펙) */
118
118
  export interface HookEventInput {
@@ -18,6 +18,7 @@
18
18
  import * as fs from 'node:fs';
19
19
  import * as path from 'node:path';
20
20
  import { execFileSync } from 'node:child_process';
21
+ import { execHost } from '../host/exec-host.js';
21
22
  import { serializeSolutionV3, DEFAULT_EVIDENCE, extractTags } from './solution-format.js';
22
23
  import { createLogger } from '../core/logger.js';
23
24
  const log = createLogger('compound-extractor');
@@ -655,15 +656,12 @@ function enrichSolutionContent(solution, diffSnippet) {
655
656
  '코드 변경 (일부):',
656
657
  diffSnippet.slice(0, 2000),
657
658
  ].join('\n');
658
- const result = execFileSync('claude', ['-p', prompt, '--model', 'haiku'], {
659
- timeout: 15000,
660
- encoding: 'utf-8',
661
- stdio: ['pipe', 'pipe', 'pipe'],
662
- });
663
- const enriched = result.trim();
664
- if (enriched.length > 30 && enriched.length < 1000) {
665
- return enriched;
666
- }
659
+ // feat/codex-support P2-2 host-aware exec via profile.default_host.
660
+ // Codex 메인 사용자도 자동 추출 enrichment 가능 (해당 host CLI 호출).
661
+ // fail-open 정책 유지 — LLM enrichment 실패는 추출 자체를 막지 않음.
662
+ const { message } = execHost({ prompt, model: 'haiku', timeout: 15000 });
663
+ if (message.length > 30 && message.length < 1000)
664
+ return message;
667
665
  return null;
668
666
  }
669
667
  catch {
@@ -147,10 +147,12 @@ function runEvolve(args) {
147
147
  const rollbackIdx = args.indexOf('--rollback');
148
148
  const promoteIdx = args.indexOf('--promote');
149
149
  if (rollbackIdx >= 0 && args[rollbackIdx + 1]) {
150
- return runEvolveRollback(args[rollbackIdx + 1]);
150
+ runEvolveRollback(args[rollbackIdx + 1]);
151
+ return;
151
152
  }
152
153
  if (promoteIdx >= 0 && args[promoteIdx + 1]) {
153
- return runEvolvePromote(args[promoteIdx + 1]);
154
+ runEvolvePromote(args[promoteIdx + 1]);
155
+ return;
154
156
  }
155
157
  // Default: generate + optionally save weakness report, print proposer
156
158
  // brief so the user can hand it to the ch-solution-evolver agent.
@@ -4,12 +4,17 @@
4
4
  * Rule.policy 자연어에서 "피해야 할 패턴" 을 추출하고, Write/Edit/Bash 도구
5
5
  * 출력에서 해당 패턴을 찾아 BypassEntry 후보로 반환한다.
6
6
  *
7
- * Heuristic:
7
+ * Heuristic priority (most explicit first):
8
+ * 0) Parenthesized examples (e.g., "(rm -rf, DROP, force-push)") → tokens inside
8
9
  * 1) "use X not Y" / "use X instead of Y" / "X over Y" → bypass = Y
9
10
  * 2) "avoid X" / "don't use X" / "never use X" / "do not use X" → bypass = X
10
11
  * 3) Korean: "X 말라" / "X 금지" / "X 하지 않" → bypass = X
11
12
  * 4) 그 외: 빈 배열 (탐지 불가).
12
13
  *
14
+ * Stop list filter: generic Korean verbs (실행/사용/선언/...) extracted by Korean
15
+ * heuristic are removed — they cause massive FP (RC5/E9: matched the word "실행"
16
+ * everywhere instead of "rm -rf"). 64 false-positive bypasses observed before fix.
17
+ *
13
18
  * 반환된 패턴은 escape 된 정규식 문자열 — caller 가 `new RegExp(p)` 로 사용.
14
19
  */
15
20
  import type { Rule } from '../../store/types.js';
@@ -4,12 +4,17 @@
4
4
  * Rule.policy 자연어에서 "피해야 할 패턴" 을 추출하고, Write/Edit/Bash 도구
5
5
  * 출력에서 해당 패턴을 찾아 BypassEntry 후보로 반환한다.
6
6
  *
7
- * Heuristic:
7
+ * Heuristic priority (most explicit first):
8
+ * 0) Parenthesized examples (e.g., "(rm -rf, DROP, force-push)") → tokens inside
8
9
  * 1) "use X not Y" / "use X instead of Y" / "X over Y" → bypass = Y
9
10
  * 2) "avoid X" / "don't use X" / "never use X" / "do not use X" → bypass = X
10
11
  * 3) Korean: "X 말라" / "X 금지" / "X 하지 않" → bypass = X
11
12
  * 4) 그 외: 빈 배열 (탐지 불가).
12
13
  *
14
+ * Stop list filter: generic Korean verbs (실행/사용/선언/...) extracted by Korean
15
+ * heuristic are removed — they cause massive FP (RC5/E9: matched the word "실행"
16
+ * everywhere instead of "rm -rf"). 64 false-positive bypasses observed before fix.
17
+ *
13
18
  * 반환된 패턴은 escape 된 정규식 문자열 — caller 가 `new RegExp(p)` 로 사용.
14
19
  */
15
20
  function escapeRegex(s) {
@@ -29,9 +34,50 @@ function trimPunct(s) {
29
34
  out = out.replace(/^[,;:!?"'`(]+|[.,;:!?"'`)]+$/g, '');
30
35
  return out;
31
36
  }
37
+ /**
38
+ * Generic Korean verbs/words that produce massive false positives if used as
39
+ * bypass patterns (RC5/E9 fix). Extending requires retro evidence.
40
+ */
41
+ const KO_GENERIC_STOP_WORDS = new Set([
42
+ '실행', '사용', '선언', '수행', '처리', '작성', '호출', '적용',
43
+ '실행하지', '사용하지', '선언하지', '수행하지', '처리하지',
44
+ // English fallthroughs (already low value as bypass signals)
45
+ 'use', 'do', 'execute',
46
+ ]);
47
+ /** Korean markers that signal the parenthesized content is NOT an example list. */
48
+ const KO_NON_EXAMPLE_MARKERS = ['제외', '한정', '예외', '단서', 'except'];
49
+ /** Extract concrete tokens inside parenthesized example list. */
50
+ function extractParenthesizedExamples(p) {
51
+ const out = [];
52
+ // Match (...) groups; multiple groups in policy are uncommon but supported
53
+ const re = /\(([^)]+)\)/g;
54
+ let m;
55
+ while ((m = re.exec(p))) {
56
+ const inside = m[1];
57
+ // Skip if it looks like a path (contains "/" before any obvious separator commitment)
58
+ if (/[a-zA-Z]+\/[a-zA-Z]/.test(inside))
59
+ continue;
60
+ // Skip if it's an exclusion / scope-restriction note (Korean markers)
61
+ if (KO_NON_EXAMPLE_MARKERS.some((mk) => inside.includes(mk)))
62
+ continue;
63
+ // Skip if any single segment is suspiciously long (full sentence rather than token)
64
+ const segs = inside.split(/[,]|\s+(?:or|와|및)\s+/i).map((s) => s.trim());
65
+ if (segs.some((s) => s.length > 30))
66
+ continue;
67
+ const tokens = segs
68
+ .map((t) => trimPunct(t))
69
+ .filter((t) => t.length >= 2 && !KO_GENERIC_STOP_WORDS.has(t));
70
+ out.push(...tokens);
71
+ }
72
+ return out;
73
+ }
32
74
  export function extractBypassPatterns(rule) {
33
75
  const patterns = [];
34
76
  const p = rule.policy;
77
+ // 0) Parenthesized examples (highest priority — explicit signal)
78
+ for (const ex of extractParenthesizedExamples(p)) {
79
+ patterns.push(escapeRegex(ex));
80
+ }
35
81
  // use X not Y / use X instead of Y / use X over Y
36
82
  // X, Y may contain dots (e.g., ".then()", "vi.mock"). Strip trailing punctuation.
37
83
  const useNot = p.match(/\b(?:use|prefer|choose)\s+(\S+?)\s+(?:not|instead\s+of|over|rather\s+than)\s+(\S+)/i);
@@ -43,10 +89,16 @@ export function extractBypassPatterns(rule) {
43
89
  patterns.push(escapeRegex(trimPunct(avoid[1])));
44
90
  // Korean: "X 말라" / "X 금지" / "X 하지 마"
45
91
  const ko = p.match(/(\S+)\s*(?:말라|금지|하지\s*마|쓰지\s*마)/);
46
- if (ko)
47
- patterns.push(escapeRegex(trimPunct(ko[1])));
48
- // Dedupe + filter trivial
49
- return [...new Set(patterns)].filter((pat) => pat.length >= 2);
92
+ if (ko) {
93
+ const candidate = trimPunct(ko[1]);
94
+ if (!KO_GENERIC_STOP_WORDS.has(candidate)) {
95
+ patterns.push(escapeRegex(candidate));
96
+ }
97
+ }
98
+ // Dedupe + filter trivial + filter stop-words (defense in depth)
99
+ return [...new Set(patterns)]
100
+ .filter((pat) => pat.length >= 2)
101
+ .filter((pat) => !KO_GENERIC_STOP_WORDS.has(pat.replace(/\\/g, '')));
50
102
  }
51
103
  /**
52
104
  * Pure — rules + tool output 으로 bypass candidates 추출.
package/dist/fgx.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import { resolveLaunchContext } from './services/session.js';
7
7
  import { prepareHarness, isFirstRun } from './core/harness.js';
8
8
  import { spawnClaude } from './core/spawn.js';
9
+ import { getHostRuntime } from './host/host-runtime.js';
9
10
  const args = process.argv.slice(2);
10
11
  // 이미 포함되어 있으면 중복 추가하지 않음
11
12
  const launchContext = resolveLaunchContext(args);
@@ -43,7 +44,7 @@ async function main() {
43
44
  console.log(`[forgen] Trust: ${v1.session.effective_trust_policy}`);
44
45
  }
45
46
  console.log('[forgen] Mode: dangerously-skip-permissions');
46
- const runtimeLabel = runtime === 'codex' ? 'Codex' : 'Claude';
47
+ const runtimeLabel = getHostRuntime(runtime).displayName;
47
48
  console.log(`[forgen] Starting ${runtimeLabel}...\n`);
48
49
  await spawnClaude(launchArgs, context, runtime);
49
50
  }
@@ -8,6 +8,7 @@
8
8
  import { createEvidence, appendEvidence } from '../store/evidence-store.js';
9
9
  import { createRule, saveRule } from '../store/rule-store.js';
10
10
  import { classify, applyProposal } from '../engine/enforce-classifier.js';
11
+ import { bumpAxisConfidence } from '../store/profile-store.js';
11
12
  // ── Correction → Evidence + Temporary Rule ──
12
13
  /**
13
14
  * 사용자 교정을 Evidence로 기록하고, 필요 시 temporary rule 생성.
@@ -31,6 +32,17 @@ export function processCorrection(req) {
31
32
  },
32
33
  });
33
34
  appendEvidence(evidence); // T1 lifecycle trigger fires here for explicit_correction
35
+ // D2 fix (2026-04-27): explicit_correction 의 axis_hint 가 axes confidence 에
36
+ // 직접 반영되도록 bump. autonomy 6건이 score 못 움직였던 결함 해결.
37
+ // 회귀 안전: facet 값은 안 건드리고 confidence 만 +0.02 (avoid-this 는 +0.04
38
+ // 로 더 강한 신호). docs/issues/D2-autonomy-facet-stuck.md 참조.
39
+ if (req.axis_hint) {
40
+ const bump = req.kind === 'avoid-this' ? 0.04 : 0.02;
41
+ try {
42
+ bumpAxisConfidence(req.axis_hint, bump);
43
+ }
44
+ catch { /* fail-open */ }
45
+ }
34
46
  // fix-now, avoid-this → temporary session rule
35
47
  let temporaryRule = null;
36
48
  if (req.kind === 'fix-now' || req.kind === 'avoid-this') {
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Forgen v1 — Onboarding
3
3
  *
4
- * 2문항 온보딩, 점수 계산, pack 추천.
5
- * Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §3-4
4
+ * 4문항 온보딩 (quality / autonomy / judgment / communication 4축), 점수 계산, pack 추천.
5
+ * Authoritative spec: docs/history/2026-04-03-tenetx-onboarding-adaptation-spec.md
6
+ * (spec §3 은 v0.1 시점 2문항 기준 — v0.4 부터 4문항으로 확장됨, src/forge/onboarding-cli.ts:69-75 참조)
6
7
  */
7
8
  import type { QualityPack, AutonomyPack, JudgmentPack, CommunicationPack, TrustPolicy, PackRecommendation } from '../store/types.js';
8
9
  export type ChoiceId = 'A' | 'B' | 'C';