@wooojin/forgen 0.4.9 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.9",
4
+ "version": "0.4.10",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",
package/dist/cli.js CHANGED
@@ -365,6 +365,14 @@ const commands = [
365
365
  await handleMigrate(args);
366
366
  },
367
367
  },
368
+ {
369
+ name: 'regress-map',
370
+ description: 'Top fix-touched files in the last N days (--days 30 --top 10 --json).',
371
+ handler: async (args) => {
372
+ const { handleRegressMap } = await import('./core/regress-map-cli.js');
373
+ await handleRegressMap(args);
374
+ },
375
+ },
368
376
  {
369
377
  name: 'suppress-rule',
370
378
  description: '[alias: rule suppress] Disable a rule by id/prefix. Hard rules refused.',
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `forgen regress-map` — fix:feat 비율 워닝을 actionable 로.
3
+ *
4
+ * doctor 의 36% 시그널을 받아 "어느 파일이 진앙인가" 를 한 화면에 보여준다.
5
+ * `--days N` (기본 30), `--top N` (기본 10), `--json` 지원.
6
+ */
7
+ export declare function handleRegressMap(args?: string[]): Promise<void>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `forgen regress-map` — fix:feat 비율 워닝을 actionable 로.
3
+ *
4
+ * doctor 의 36% 시그널을 받아 "어느 파일이 진앙인가" 를 한 화면에 보여준다.
5
+ * `--days N` (기본 30), `--top N` (기본 10), `--json` 지원.
6
+ */
7
+ import { computeRegressMap, formatFixRatio, computeFixFeatRatio } from './git-stats.js';
8
+ function parseIntArg(args, flag, fallback) {
9
+ const i = args.indexOf(flag);
10
+ if (i === -1)
11
+ return fallback;
12
+ const v = Number.parseInt(args[i + 1] ?? '', 10);
13
+ return Number.isFinite(v) && v > 0 ? v : fallback;
14
+ }
15
+ export async function handleRegressMap(args = []) {
16
+ const days = parseIntArg(args, '--days', 30);
17
+ const top = parseIntArg(args, '--top', 10);
18
+ const asJson = args.includes('--json');
19
+ const map = computeRegressMap(process.cwd(), days, top);
20
+ const ratio = computeFixFeatRatio(process.cwd(), 30);
21
+ if (asJson) {
22
+ process.stdout.write(JSON.stringify({ fixFeat: ratio, regress: map }, null, 2));
23
+ process.stdout.write('\n');
24
+ return;
25
+ }
26
+ if (!map.available) {
27
+ console.log('regress-map: git unavailable or no commits in window.');
28
+ return;
29
+ }
30
+ console.log(' ┌─ forgen regress-map ───────────────────────────────────┐');
31
+ console.log(` │ window: last ${map.windowDays} days · fix commits: ${map.fixCommits}`.padEnd(60) + '│');
32
+ if (ratio.available) {
33
+ console.log(` │ ${formatFixRatio(ratio)}`.padEnd(60) + '│');
34
+ }
35
+ console.log(' ├────────────────────────────────────────────────────────┤');
36
+ if (map.hotspots.length === 0) {
37
+ console.log(' │ No fix-touched files in window.'.padEnd(60) + '│');
38
+ }
39
+ else {
40
+ console.log(' │ rank hits file (last fix · sha)'.padEnd(60) + '│');
41
+ map.hotspots.forEach((h, i) => {
42
+ const rank = String(i + 1).padStart(2, ' ');
43
+ const hits = String(h.fixHits).padStart(3, ' ');
44
+ const meta = `${h.lastFixDate} ${h.lastFixSha}`;
45
+ const pathBudget = 56 - 4 - 4 - meta.length - 3;
46
+ const shownPath = h.path.length > pathBudget
47
+ ? '…' + h.path.slice(-(pathBudget - 1))
48
+ : h.path;
49
+ const line = ` ${rank} ${hits} ${shownPath} (${meta})`;
50
+ console.log(` │ ${line}`.padEnd(60) + '│');
51
+ });
52
+ }
53
+ console.log(' └────────────────────────────────────────────────────────┘');
54
+ if (ratio.available && ratio.exceedsThreshold) {
55
+ console.log('');
56
+ console.log(' ⚠ fix:feat 비율 초과 — 상위 파일의 invariant/테스트 보강 권장.');
57
+ console.log(' → 같은 파일이 3회 이상 fix 닿았다면 책임 분리 또는 회귀 테스트 우선.');
58
+ }
59
+ }
package/dist/fgx.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * fgx — forgen --dangerously-skip-permissions 의 단축 명령
4
- * 모든 인자를 그대로 전달하되, --dangerously-skip-permissions 를 자동 주입
4
+ *
5
+ * 기본 동작: 모든 인자를 Claude(또는 Codex) 로 그대로 전달 + 권한 우회 플래그 자동 주입.
6
+ * v0.4.10: forgen 서브커맨드(status/init/install/maintenance/compound 등)는
7
+ * Claude 로 안 넘기고 cli.js 로 라우팅 (사용자 혼동 갭 해소).
5
8
  */
6
9
  export {};
package/dist/fgx.js CHANGED
@@ -1,22 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * fgx — forgen --dangerously-skip-permissions 의 단축 명령
4
- * 모든 인자를 그대로 전달하되, --dangerously-skip-permissions 를 자동 주입
4
+ *
5
+ * 기본 동작: 모든 인자를 Claude(또는 Codex) 로 그대로 전달 + 권한 우회 플래그 자동 주입.
6
+ * v0.4.10: forgen 서브커맨드(status/init/install/maintenance/compound 등)는
7
+ * Claude 로 안 넘기고 cli.js 로 라우팅 (사용자 혼동 갭 해소).
5
8
  */
6
9
  import { resolveLaunchContext } from './services/session.js';
7
10
  import { prepareHarness, isFirstRun } from './core/harness.js';
8
11
  import { spawnClaude } from './core/spawn.js';
9
12
  import { getHostRuntime } from './host/host-runtime.js';
10
13
  const args = process.argv.slice(2);
11
- // 이미 포함되어 있으면 중복 추가하지 않음
12
- const launchContext = resolveLaunchContext(args);
13
- const runtime = launchContext.runtime;
14
- const skipFlag = getHostRuntime(runtime).dangerousSkipFlag;
15
- const launchArgs = [...launchContext.args];
16
- if (!launchArgs.includes(skipFlag)) {
17
- launchArgs.unshift(skipFlag);
14
+ // v0.4.10: forgen 서브커맨드 인벤토리. src/cli.ts 의 commands[] 와 sync 유지.
15
+ // 첫 비-플래그 인자가 이 집합에 들어가면 forgen cli 로 라우팅.
16
+ const FORGEN_SUBCOMMANDS = new Set([
17
+ 'forge', 'compound', 'skill', 'dashboard', 'learn', 'me', 'statusline',
18
+ 'config', 'mcp', 'init', 'install', 'status', 'maintenance', 'parity',
19
+ 'notepad', 'inspect', 'onboarding', 'doctor', 'uninstall', 'rule',
20
+ 'classify-enforce', 'rule-meta-scan', 'lifecycle-scan',
21
+ 'stats', 'last-block', 'recall', 'migrate', 'suppress-rule', 'activate-rule',
22
+ 'regress-map',
23
+ // 메타 명령도 cli.ts 가 처리 (fgx claude spawn 으로는 의미 없음)
24
+ 'help', '--help', '-h', '--version', '-V',
25
+ ]);
26
+ function findFirstSubcommand(rawArgs) {
27
+ for (const a of rawArgs) {
28
+ if (FORGEN_SUBCOMMANDS.has(a))
29
+ return a;
30
+ // 첫 비-플래그 인자를 보면 더 이상 후보 X (e.g. ['my-prompt', 'status'] → null)
31
+ if (!a.startsWith('-'))
32
+ return null;
33
+ }
34
+ return null;
18
35
  }
19
- async function main() {
36
+ async function routeToCli() {
37
+ // cli.js 가 process.argv.slice(2) 를 직접 사용. fgx 가 받은 args 그대로 전달됨.
38
+ // top-level main() 이 모듈 로드 시 실행 + process.exit 처리.
39
+ await import('./cli.js');
40
+ }
41
+ async function runClaudeLauncher() {
42
+ const launchContext = resolveLaunchContext(args);
43
+ const runtime = launchContext.runtime;
44
+ const skipFlag = getHostRuntime(runtime).dangerousSkipFlag;
45
+ const launchArgs = [...launchContext.args];
46
+ if (!launchArgs.includes(skipFlag)) {
47
+ launchArgs.unshift(skipFlag);
48
+ }
20
49
  // Security warning — fgx bypasses all Claude Code permission checks.
21
50
  //
22
51
  // Audit fix #3 (2026-04-21): The warning banner is shown regardless of
@@ -28,7 +57,6 @@ async function main() {
28
57
  console.warn(` ⚠ ${getHostRuntime(runtime).displayName} will execute tools without asking for confirmation.`);
29
58
  console.warn(' ⚠ Use only in trusted environments. If your profile trust policy is');
30
59
  console.warn(' ⚠ "가드레일 우선" or "승인 완화", consider `forgen` (no flag) instead.\n');
31
- // fgx는 서브커맨드 없이 바로 Claude Code 실행 전용
32
60
  const firstRun = isFirstRun();
33
61
  if (firstRun) {
34
62
  console.log('\n Forgen — Setting up for the first time.\n');
@@ -49,6 +77,14 @@ async function main() {
49
77
  console.log(`[forgen] Starting ${runtimeLabel}...\n`);
50
78
  await spawnClaude(launchArgs, context, runtime);
51
79
  }
80
+ async function main() {
81
+ const sub = findFirstSubcommand(args);
82
+ if (sub !== null) {
83
+ await routeToCli();
84
+ return;
85
+ }
86
+ await runClaudeLauncher();
87
+ }
52
88
  main().catch((err) => {
53
89
  console.error('[forgen] Error:', err instanceof Error ? err.message : err);
54
90
  process.exit(1);
@@ -20,10 +20,14 @@ function evidencePath(evidenceId) {
20
20
  /**
21
21
  * 현재 세션이 어느 host 에서 실행되는지 추론 (Multi-Host §4.2).
22
22
  * 1) explicit `params.host`
23
- * 2) env var `FORGEN_HOST` (e2e 격리용)
24
- * 3) `runtime` env hint
23
+ * 2) env var `FORGEN_HOST` (e2e 격리용 / MCP server 가 --host=codex 받으면 set)
24
+ * 3) `FORGEN_RUNTIME` env (config-injector.ts:479 가 harness spawn 시 주입)
25
25
  * 4) Codex CLI 흔적 (`CODEX_HOME` 또는 `CODEX_SANDBOX_NETWORK_DISABLED`)
26
26
  * 5) default 'claude' (1원칙)
27
+ *
28
+ * v0.4.10 fix: 3번 fallback 이 주석에만 있고 코드에 없어서 Codex harness 세션의
29
+ * auto-compound-runner 가 생성하는 behavior_observation evidence 가 전부 'claude'
30
+ * 로 잘못 태깅 → doctor 가 98/2 격차 경고. FORGEN_RUNTIME 을 직접 읽도록 추가.
27
31
  */
28
32
  function detectHost(explicit) {
29
33
  if (explicit)
@@ -31,6 +35,9 @@ function detectHost(explicit) {
31
35
  const fromEnv = process.env.FORGEN_HOST;
32
36
  if (fromEnv === 'claude' || fromEnv === 'codex')
33
37
  return fromEnv;
38
+ const fromRuntime = process.env.FORGEN_RUNTIME;
39
+ if (fromRuntime === 'claude' || fromRuntime === 'codex')
40
+ return fromRuntime;
34
41
  if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX_NETWORK_DISABLED)
35
42
  return 'codex';
36
43
  return 'claude';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooojin/forgen",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "preferGlobal": true,
5
5
  "main": "dist/lib.js",
6
6
  "types": "./dist/lib.d.ts",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.9",
4
+ "version": "0.4.10",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",