@wooojin/forgen 0.4.6 → 0.4.8

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 (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +78 -0
  3. package/dist/checks/self-score-deflation.js +6 -4
  4. package/dist/cli.js +6 -3
  5. package/dist/core/auto-compound-runner.js +6 -2
  6. package/dist/core/dashboard.js +2 -2
  7. package/dist/core/doctor.d.ts +10 -0
  8. package/dist/core/doctor.js +49 -8
  9. package/dist/core/harness.js +8 -2
  10. package/dist/core/inspect-cli.js +4 -4
  11. package/dist/core/migrate-evidence-host.js +1 -1
  12. package/dist/core/notify.js +7 -0
  13. package/dist/core/paths.d.ts +16 -2
  14. package/dist/core/paths.js +16 -2
  15. package/dist/core/session-store.d.ts +12 -1
  16. package/dist/core/session-store.js +73 -1
  17. package/dist/core/spawn.js +13 -7
  18. package/dist/core/v1-bootstrap.d.ts +7 -0
  19. package/dist/core/v1-bootstrap.js +28 -6
  20. package/dist/engine/compound-extractor.js +1 -1
  21. package/dist/engine/learn-cli.js +2 -2
  22. package/dist/engine/lifecycle/bypass-detector.js +3 -2
  23. package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
  24. package/dist/engine/lifecycle/signals.js +2 -2
  25. package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
  26. package/dist/engine/solution-candidate.js +1 -1
  27. package/dist/engine/solution-outcomes.js +1 -1
  28. package/dist/engine/solution-quarantine.js +1 -1
  29. package/dist/engine/solution-weakness.js +8 -2
  30. package/dist/fgx.js +6 -5
  31. package/dist/forge/cli.js +1 -1
  32. package/dist/hooks/keyword-detector.js +1 -1
  33. package/dist/hooks/secret-filter.js +2 -2
  34. package/dist/hooks/shared/hook-response.js +1 -1
  35. package/dist/hooks/shared/hook-timing.js +3 -3
  36. package/dist/hooks/solution-injector.js +1 -1
  37. package/dist/hooks/stop-guard.js +3 -3
  38. package/dist/host/host-runtime.d.ts +6 -0
  39. package/dist/host/host-runtime.js +2 -0
  40. package/dist/host/install-codex.js +1 -1
  41. package/dist/host/install-orchestrator.js +2 -1
  42. package/dist/mcp/tools.js +1 -1
  43. package/dist/preset/facet-catalog.js +2 -2
  44. package/dist/renderer/rule-renderer.js +7 -7
  45. package/dist/store/compound-usage-store.js +1 -1
  46. package/dist/store/implicit-feedback-store.js +2 -2
  47. package/dist/store/profile-store.d.ts +11 -0
  48. package/dist/store/profile-store.js +23 -0
  49. package/package.json +1 -1
  50. package/plugin.json +1 -1
@@ -15,10 +15,10 @@
15
15
  import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import * as crypto from 'node:crypto';
18
- import { FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, STATE_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS } from './paths.js';
18
+ import { FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, STATE_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS, SESSIONS_DIR } from './paths.js';
19
19
  import { checkLegacyProfile, runLegacyCutover } from './legacy-detector.js';
20
20
  import { detectRuntimeCapability } from './runtime-detector.js';
21
- import { loadProfile, profileExists } from '../store/profile-store.js';
21
+ import { backupCorruptProfile, loadProfile, profileExists } from '../store/profile-store.js';
22
22
  import { loadActiveRules, cleanupStaleSessionRules, markRulesInjected } from '../store/rule-store.js';
23
23
  import { composeSession } from '../preset/preset-manager.js';
24
24
  import { renderRules, DEFAULT_CONTEXT } from '../renderer/rule-renderer.js';
@@ -27,7 +27,14 @@ import { loadEvidenceBySession } from '../store/evidence-store.js';
27
27
  import { computeSessionSignals, detectMismatch } from '../forge/mismatch-detector.js';
28
28
  import { createRecommendation, saveRecommendation } from '../store/recommendation-store.js';
29
29
  // ── Directory Initialization ──
30
- const V1_DIRS = [FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, STATE_DIR, V1_SESSIONS_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS];
30
+ // v0.4.8 (A3): SESSIONS_DIR (~/.forgen/sessions/) v1 bootstrap 보장 대상.
31
+ // 이전엔 V1_DIRS 에 누락되어 있어, prepareHarness step 11 (startSessionLog)
32
+ // 에 도달 못 하는 코드 경로 (예: forgen install 직접 실행) 에선 디렉토리가
33
+ // 끝까지 생성되지 않았음. legacy session log 와 v1 effective state 는
34
+ // 서로 다른 저장소 책임이라 두 dir 모두 명시 보장.
35
+ // - SESSIONS_DIR = ~/.forgen/sessions/ ← legacy session log (transcript-like)
36
+ // - V1_SESSIONS_DIR = ~/.forgen/state/sessions/ ← v1 effective state per session
37
+ const V1_DIRS = [FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, STATE_DIR, V1_SESSIONS_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS, SESSIONS_DIR];
31
38
  export function ensureV1Directories() {
32
39
  for (const dir of V1_DIRS) {
33
40
  fs.mkdirSync(dir, { recursive: true });
@@ -48,6 +55,7 @@ export function bootstrapV1Session() {
48
55
  return {
49
56
  needsOnboarding: true,
50
57
  legacyBackupPath,
58
+ corruptProfileBackupPath: null,
51
59
  session: null,
52
60
  renderedRules: null,
53
61
  profile: null,
@@ -56,7 +64,20 @@ export function bootstrapV1Session() {
56
64
  }
57
65
  const profile = loadProfile();
58
66
  if (!profile) {
59
- return { needsOnboarding: true, legacyBackupPath, session: null, renderedRules: null, profile: null, mismatch: null };
67
+ // v0.4.8 corrupt/invalid profile auto-repair.
68
+ // profileExists()=true && loadProfile()=null 은 parse 실패 또는 v1
69
+ // shape 위반. 그대로 두면 다음 실행에서도 같은 분기로 빠지므로
70
+ // backup 후 새 onboarding 흐름을 강제.
71
+ const corruptProfileBackupPath = backupCorruptProfile();
72
+ return {
73
+ needsOnboarding: true,
74
+ legacyBackupPath,
75
+ corruptProfileBackupPath,
76
+ session: null,
77
+ renderedRules: null,
78
+ profile: null,
79
+ mismatch: null,
80
+ };
60
81
  }
61
82
  // 3. Runtime capability 감지
62
83
  const runtime = detectRuntimeCapability();
@@ -133,7 +154,7 @@ export function bootstrapV1Session() {
133
154
  try {
134
155
  // 세션 시작 로그
135
156
  const rawLogPath = path.join(V1_RAW_LOGS_DIR, `${sessionId}.jsonl`);
136
- fs.appendFileSync(rawLogPath, JSON.stringify({
157
+ fs.appendFileSync(rawLogPath, `${JSON.stringify({
137
158
  event: 'session-started',
138
159
  session_id: sessionId,
139
160
  timestamp: new Date().toISOString(),
@@ -142,7 +163,7 @@ export function bootstrapV1Session() {
142
163
  judgment_pack: profile.base_packs.judgment_pack,
143
164
  communication_pack: profile.base_packs.communication_pack,
144
165
  effective_trust: session.effective_trust_policy,
145
- }) + '\n');
166
+ })}\n`);
146
167
  // TTL sweep: 7일 이상 된 raw log 파일 삭제
147
168
  const TTL_MS = 7 * 24 * 60 * 60 * 1000;
148
169
  const now = Date.now();
@@ -163,6 +184,7 @@ export function bootstrapV1Session() {
163
184
  return {
164
185
  needsOnboarding: false,
165
186
  legacyBackupPath,
187
+ corruptProfileBackupPath: null,
166
188
  session,
167
189
  renderedRules,
168
190
  profile,
@@ -557,7 +557,7 @@ function findCommonPrefix(strings) {
557
557
  return prefix.replace(/-$/, '');
558
558
  }
559
559
  /** Save an extracted solution as experiment */
560
- function saveExtractedSolution(sol, sessionId) {
560
+ function saveExtractedSolution(sol, _sessionId) {
561
561
  const today = new Date().toISOString().split('T')[0];
562
562
  const slugName = sol.name.toLowerCase()
563
563
  .replace(/[^a-z0-9가-힣\s-]/g, '')
@@ -136,7 +136,7 @@ function runFitness(args) {
136
136
  console.log(` ${'name'.padEnd(48)} ${'state'.padEnd(14)} ${'inj'.padStart(4)} ${'acc/cor/err'.padStart(11)} ${'fit'.padStart(6)}`);
137
137
  console.log(` ${'-'.repeat(48)} ${'-'.repeat(14)} ${'-'.repeat(4)} ${'-'.repeat(11)} ${'-'.repeat(6)}`);
138
138
  for (const r of records) {
139
- const name = r.solution.length > 47 ? r.solution.slice(0, 45) + '..' : r.solution;
139
+ const name = r.solution.length > 47 ? `${r.solution.slice(0, 45)}..` : r.solution;
140
140
  const acr = `${r.accepted}/${r.corrected}/${r.errored}`;
141
141
  console.log(` ${name.padEnd(48)} ${r.state.padEnd(14)} ${String(r.injected).padStart(4)} ${acr.padStart(11)} ${r.fitness.toFixed(2).padStart(6)}`);
142
142
  }
@@ -222,7 +222,7 @@ function runEvolvePromote(candidateNameOrList) {
222
222
  }
223
223
  const result = promoteCandidate(candidateNameOrList);
224
224
  if (result.ok) {
225
- console.log(`\n ✓ Promoted: ${path.basename(result.dest)}`);
225
+ console.log(`\n ✓ Promoted: ${result.dest ? path.basename(result.dest) : '(unknown)'}`);
226
226
  console.log(` from: ${result.source}`);
227
227
  console.log(` to: ${result.dest}`);
228
228
  console.log(` Cold-start bonus active until 5 injections accumulate (auto-promotes to verified).\n`);
@@ -51,9 +51,10 @@ function extractParenthesizedExamples(p) {
51
51
  const out = [];
52
52
  // Match (...) groups; multiple groups in policy are uncommon but supported
53
53
  const re = /\(([^)]+)\)/g;
54
- let m;
55
- while ((m = re.exec(p))) {
54
+ let m = re.exec(p);
55
+ while (m !== null) {
56
56
  const inside = m[1];
57
+ m = re.exec(p);
57
58
  // Skip if it looks like a path (contains "/" before any obvious separator commitment)
58
59
  if (/[a-zA-Z]+\/[a-zA-Z]/.test(inside))
59
60
  continue;
@@ -294,7 +294,7 @@ export function appendLifecycleEvents(events, now = Date.now()) {
294
294
  }
295
295
  }
296
296
  catch { /* missing → no rotate */ }
297
- const body = events.map((e) => JSON.stringify(e)).join('\n') + '\n';
297
+ const body = `${events.map((e) => JSON.stringify(e)).join('\n')}\n`;
298
298
  fs.appendFileSync(logPath, body);
299
299
  }
300
300
  catch (e) {
@@ -62,7 +62,7 @@ export function recordViolation(entry) {
62
62
  fs.mkdirSync(ENFORCEMENT_DIR, { recursive: true });
63
63
  rotateIfBig(VIOLATIONS_PATH);
64
64
  const full = { at: new Date().toISOString(), ...entry };
65
- fs.appendFileSync(VIOLATIONS_PATH, JSON.stringify(full) + '\n');
65
+ fs.appendFileSync(VIOLATIONS_PATH, `${JSON.stringify(full)}\n`);
66
66
  }
67
67
  catch (e) {
68
68
  // best-effort, 실패 시 debug 로그 (silent swallow 방지)
@@ -76,7 +76,7 @@ export function recordBypass(entry) {
76
76
  fs.mkdirSync(ENFORCEMENT_DIR, { recursive: true });
77
77
  rotateIfBig(BYPASS_PATH);
78
78
  const full = { at: new Date().toISOString(), ...entry };
79
- fs.appendFileSync(BYPASS_PATH, JSON.stringify(full) + '\n');
79
+ fs.appendFileSync(BYPASS_PATH, `${JSON.stringify(full)}\n`);
80
80
  }
81
81
  catch (e) {
82
82
  if (process.env.FORGEN_DEBUG_SIGNALS === '1') {
@@ -39,7 +39,7 @@ function matchesRule(evidence, rule) {
39
39
  .toLowerCase();
40
40
  return keyTokens.some((t) => {
41
41
  const tokLower = t.toLowerCase();
42
- return summaryLower.includes(tokLower) || (targetToken && targetToken.includes(tokLower));
42
+ return summaryLower.includes(tokLower) || (targetToken?.includes(tokLower));
43
43
  });
44
44
  }
45
45
  export function detect(input) {
@@ -100,7 +100,7 @@ export function rollbackSince(epochMs) {
100
100
  continue;
101
101
  try {
102
102
  fs.mkdirSync(archiveDir, { recursive: true });
103
- const destName = path.basename(dir) + '__' + file;
103
+ const destName = `${path.basename(dir)}__${file}`;
104
104
  fs.renameSync(filePath, path.join(archiveDir, destName));
105
105
  archived.push(filePath);
106
106
  }
@@ -30,7 +30,7 @@ function writePending(sessionId, state) {
30
30
  }
31
31
  function appendOutcome(event) {
32
32
  fs.mkdirSync(OUTCOMES_DIR, { recursive: true });
33
- fs.appendFileSync(outcomesPath(event.session_id), JSON.stringify(event) + '\n');
33
+ fs.appendFileSync(outcomesPath(event.session_id), `${JSON.stringify(event)}\n`);
34
34
  }
35
35
  /**
36
36
  * Run a read-modify-write pending-state mutation under a file lock
@@ -49,7 +49,7 @@ export function recordQuarantine(filePath, errors) {
49
49
  at: new Date().toISOString(),
50
50
  errors,
51
51
  };
52
- fs.appendFileSync(SOLUTION_QUARANTINE_PATH, JSON.stringify(entry) + '\n');
52
+ fs.appendFileSync(SOLUTION_QUARANTINE_PATH, `${JSON.stringify(entry)}\n`);
53
53
  }
54
54
  catch (e) {
55
55
  log.debug(`quarantine write failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -97,14 +97,20 @@ function findConflictClusters(rows, fitnessByName) {
97
97
  const underperformers = rows.filter((r) => fitnessByName.get(r.name)?.state === 'underperform');
98
98
  const clusters = [];
99
99
  for (const ch of champions) {
100
+ const chFitness = fitnessByName.get(ch.name)?.fitness;
101
+ if (chFitness === undefined)
102
+ continue;
100
103
  for (const up of underperformers) {
104
+ const upFitness = fitnessByName.get(up.name)?.fitness;
105
+ if (upFitness === undefined)
106
+ continue;
101
107
  const shared = ch.tags.filter((t) => up.tags.includes(t));
102
108
  if (shared.length < 2)
103
109
  continue;
104
110
  clusters.push({
105
111
  shared_tags: shared,
106
- champion: { name: ch.name, fitness: fitnessByName.get(ch.name).fitness },
107
- underperform: { name: up.name, fitness: fitnessByName.get(up.name).fitness },
112
+ champion: { name: ch.name, fitness: chFitness },
113
+ underperform: { name: up.name, fitness: upFitness },
108
114
  });
109
115
  }
110
116
  }
package/dist/fgx.js CHANGED
@@ -11,9 +11,10 @@ const args = process.argv.slice(2);
11
11
  // 이미 포함되어 있으면 중복 추가하지 않음
12
12
  const launchContext = resolveLaunchContext(args);
13
13
  const runtime = launchContext.runtime;
14
+ const skipFlag = getHostRuntime(runtime).dangerousSkipFlag;
14
15
  const launchArgs = [...launchContext.args];
15
- if (!launchArgs.includes('--dangerously-skip-permissions')) {
16
- launchArgs.unshift('--dangerously-skip-permissions');
16
+ if (!launchArgs.includes(skipFlag)) {
17
+ launchArgs.unshift(skipFlag);
17
18
  }
18
19
  async function main() {
19
20
  // Security warning — fgx bypasses all Claude Code permission checks.
@@ -23,8 +24,8 @@ async function main() {
23
24
  // alias `fgx` unknowingly run with zero guardrails. Users who rely on
24
25
  // the profile trust policy should NOT use `fgx`. Surface the mismatch
25
26
  // loudly (harness.ts also prints the Trust 상승 warning downstream).
26
- console.warn('\n ⚠ fgx: ALL permission checks are disabled (--dangerously-skip-permissions)');
27
- console.warn('Claude Code will execute tools without asking for confirmation.');
27
+ console.warn(`\n ⚠ fgx: ALL permission checks are disabled (${skipFlag})`);
28
+ console.warn(`${getHostRuntime(runtime).displayName} will execute tools without asking for confirmation.`);
28
29
  console.warn(' ⚠ Use only in trusted environments. If your profile trust policy is');
29
30
  console.warn(' ⚠ "가드레일 우선" or "승인 완화", consider `forgen` (no flag) instead.\n');
30
31
  // fgx는 서브커맨드 없이 바로 Claude Code 실행 전용
@@ -43,7 +44,7 @@ async function main() {
43
44
  if (v1.session) {
44
45
  console.log(`[forgen] Trust: ${v1.session.effective_trust_policy}`);
45
46
  }
46
- console.log('[forgen] Mode: dangerously-skip-permissions');
47
+ console.log(`[forgen] Mode: ${skipFlag.replace(/^--/, '')}`);
47
48
  const runtimeLabel = getHostRuntime(runtime).displayName;
48
49
  console.log(`[forgen] Starting ${runtimeLabel}...\n`);
49
50
  await spawnClaude(launchArgs, context, runtime);
package/dist/forge/cli.js CHANGED
@@ -39,7 +39,7 @@ function handleShowProfile() {
39
39
  console.log('\n No v1 profile found. Run `forgen forge` or `forgen onboarding`.\n');
40
40
  return;
41
41
  }
42
- console.log('\n' + renderProfile(profile) + '\n');
42
+ console.log(`\n${renderProfile(profile)}\n`);
43
43
  }
44
44
  function handleExport() {
45
45
  const profile = loadProfile();
@@ -192,7 +192,7 @@ async function main() {
192
192
  return;
193
193
  }
194
194
  const match = detectKeyword(input.prompt);
195
- const sessionId = input.session_id ?? 'unknown';
195
+ const _sessionId = input.session_id ?? 'unknown';
196
196
  // v1: regex 기반 prompt 학습 제거. Evidence 기반으로 전환됨.
197
197
  if (!match) {
198
198
  console.log(approve());
@@ -34,10 +34,10 @@ export function redactSecrets(text) {
34
34
  let out = text;
35
35
  for (const sp of SECRET_PATTERNS) {
36
36
  // regex 복제 (global flag 없이 repeated test 되는 경우 lastIndex 안전)
37
- const re = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : sp.pattern.flags + 'g'));
37
+ const re = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : `${sp.pattern.flags}g`));
38
38
  if (re.test(out)) {
39
39
  hits.push(sp);
40
- const re2 = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : sp.pattern.flags + 'g'));
40
+ const re2 = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : `${sp.pattern.flags}g`));
41
41
  out = out.replace(re2, `[REDACTED:${sp.name}]`);
42
42
  }
43
43
  }
@@ -146,7 +146,7 @@ export function failOpenWithTracking(hookName, err) {
146
146
  }
147
147
  }
148
148
  const entry = JSON.stringify(payload);
149
- fs.appendFileSync(logPath, entry + '\n');
149
+ fs.appendFileSync(logPath, `${entry}\n`);
150
150
  }
151
151
  catch { /* fail-open: tracking itself must not throw */ }
152
152
  return JSON.stringify({ continue: true });
@@ -19,7 +19,7 @@ export function recordHookTiming(hookName, durationMs, event) {
19
19
  try {
20
20
  fs.mkdirSync(STATE_DIR, { recursive: true });
21
21
  const entry = JSON.stringify({ hook: hookName, ms: durationMs, event, at: Date.now() });
22
- fs.appendFileSync(TIMING_LOG, entry + '\n');
22
+ fs.appendFileSync(TIMING_LOG, `${entry}\n`);
23
23
  // Rotate if too large — size-gated (statSync only, skip read/write 대부분의 호출)
24
24
  try {
25
25
  const size = fs.statSync(TIMING_LOG).size;
@@ -28,7 +28,7 @@ export function recordHookTiming(hookName, durationMs, event) {
28
28
  const content = fs.readFileSync(TIMING_LOG, 'utf-8');
29
29
  const lines = content.trim().split('\n');
30
30
  if (lines.length > MAX_LINES) {
31
- fs.writeFileSync(TIMING_LOG, lines.slice(-MAX_LINES).join('\n') + '\n');
31
+ fs.writeFileSync(TIMING_LOG, `${lines.slice(-MAX_LINES).join('\n')}\n`);
32
32
  }
33
33
  }
34
34
  catch { /* skip rotation on error */ }
@@ -52,7 +52,7 @@ export function getTimingStats() {
52
52
  for (const e of entries) {
53
53
  if (!byHook.has(e.hook))
54
54
  byHook.set(e.hook, []);
55
- byHook.get(e.hook).push(e.ms);
55
+ byHook.get(e.hook)?.push(e.ms);
56
56
  }
57
57
  const stats = [];
58
58
  for (const [hook, times] of byHook) {
@@ -409,7 +409,7 @@ async function main() {
409
409
  .filter(l => l.length > 0);
410
410
  contentSnippet = lines.slice(0, 3).join('\n');
411
411
  if (contentSnippet.length > SUMMARY_MAX_CHARS) {
412
- contentSnippet = contentSnippet.slice(0, SUMMARY_MAX_CHARS - 3) + '...';
412
+ contentSnippet = `${contentSnippet.slice(0, SUMMARY_MAX_CHARS - 3)}...`;
413
413
  }
414
414
  }
415
415
  }
@@ -370,14 +370,14 @@ export function acknowledgeSessionBlocks(sessionId) {
370
370
  try {
371
371
  fs.mkdirSync(path.dirname(ACK_LOG), { recursive: true });
372
372
  rotateIfBig(ACK_LOG);
373
- fs.appendFileSync(ACK_LOG, JSON.stringify({
373
+ fs.appendFileSync(ACK_LOG, `${JSON.stringify({
374
374
  at: now,
375
375
  session_id: state.sessionId,
376
376
  rule_id: state.ruleId,
377
377
  block_count: state.count,
378
378
  first_block_at: state.firstBlockAt,
379
379
  last_block_at: state.lastBlockAt,
380
- }) + '\n');
380
+ })}\n`);
381
381
  acked += 1;
382
382
  }
383
383
  catch { /* append failure: still try cleanup */ }
@@ -396,7 +396,7 @@ export function logDriftEvent(event) {
396
396
  try {
397
397
  fs.mkdirSync(path.dirname(DRIFT_LOG), { recursive: true });
398
398
  rotateIfBig(DRIFT_LOG);
399
- fs.appendFileSync(DRIFT_LOG, JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n');
399
+ fs.appendFileSync(DRIFT_LOG, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`);
400
400
  }
401
401
  catch {
402
402
  // best-effort
@@ -33,5 +33,11 @@ export interface HostRuntime {
33
33
  * - 'pre-baked-file': pkgRoot/hooks/hooks.json 읽고 ${CLAUDE_PLUGIN_ROOT} 치환 (Claude — 빌드 산출물 재사용)
34
34
  */
35
35
  readonly hookInjectionStrategy: 'generate' | 'pre-baked-file';
36
+ /**
37
+ * 권한 전수 우회용 CLI 플래그 (fgx 등 dangerously-skip 모드에서 사용).
38
+ * Claude: --dangerously-skip-permissions
39
+ * Codex: --dangerously-bypass-approvals-and-sandbox
40
+ */
41
+ readonly dangerousSkipFlag: string;
36
42
  }
37
43
  export declare function getHostRuntime(runtime: RuntimeHost): HostRuntime;
@@ -25,6 +25,7 @@ const claudeRuntime = {
25
25
  return args ? `node ${quoteArg(fullScript)} ${args}` : `node ${quoteArg(fullScript)}`;
26
26
  },
27
27
  hookInjectionStrategy: 'pre-baked-file',
28
+ dangerousSkipFlag: '--dangerously-skip-permissions',
28
29
  };
29
30
  const codexRuntime = {
30
31
  id: 'codex',
@@ -38,6 +39,7 @@ const codexRuntime = {
38
39
  return args ? `${base} ${args}` : base;
39
40
  },
40
41
  hookInjectionStrategy: 'generate',
42
+ dangerousSkipFlag: '--dangerously-bypass-approvals-and-sandbox',
41
43
  };
42
44
  const RUNTIMES = {
43
45
  claude: claudeRuntime,
@@ -202,7 +202,7 @@ function installCodexSkills(opts) {
202
202
  return { installed: count };
203
203
  }
204
204
  // ── P3-3: AGENTS.md inject ────────────────────────────────────────────
205
- function resolveAgentsMdPath(pkgRoot) {
205
+ function resolveAgentsMdPath(_pkgRoot) {
206
206
  // Phase 3 critic fix: pkgRoot 기반 walk-up 은 `npm install -g` 시 시스템 디렉토리
207
207
  // (예: /usr/local/lib/node_modules/forgen) 에 fallback AGENTS.md 작성 위험.
208
208
  // *cwd 기반* 으로 변경 — 사용자 작업 디렉토리의 git root, 없으면 cwd 자체.
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import * as path from 'node:path';
14
14
  import * as readline from 'node:readline';
15
+ import { fileURLToPath } from 'node:url';
15
16
  import { detectAvailableHosts } from '../core/host-detect.js';
16
17
  import { planClaudeInstall } from './install-claude.js';
17
18
  import { planCodexInstall } from './install-codex.js';
@@ -121,6 +122,6 @@ export function renderResult(result, dryRun) {
121
122
  }
122
123
  /** pkgRoot resolve from binary location (dist/cli.js → pkgRoot). */
123
124
  export function resolvePkgRootFromBinary(metaUrl) {
124
- const here = path.dirname(new URL(metaUrl).pathname);
125
+ const here = path.dirname(fileURLToPath(metaUrl));
125
126
  return path.resolve(here, '..');
126
127
  }
package/dist/mcp/tools.js CHANGED
@@ -239,7 +239,7 @@ export function registerTools(server) {
239
239
  }],
240
240
  };
241
241
  }
242
- catch (e) {
242
+ catch (_e) {
243
243
  return {
244
244
  content: [{
245
245
  type: 'text',
@@ -29,8 +29,8 @@ export const COMMUNICATION_CENTROIDS = {
29
29
  '상세형': { verbosity: 0.85, structure: 0.80, teaching_bias: 0.80 },
30
30
  };
31
31
  // ── Defaults (backward compat) ──
32
- export const DEFAULT_JUDGMENT_FACETS = JUDGMENT_CENTROIDS['균형형'];
33
- export const DEFAULT_COMMUNICATION_FACETS = COMMUNICATION_CENTROIDS['균형형'];
32
+ export const DEFAULT_JUDGMENT_FACETS = JUDGMENT_CENTROIDS.균형형;
33
+ export const DEFAULT_COMMUNICATION_FACETS = COMMUNICATION_CENTROIDS.균형형;
34
34
  // ── Utilities ──
35
35
  export function qualityCentroid(pack) {
36
36
  return { ...QUALITY_CENTROIDS[pack] };
@@ -171,28 +171,28 @@ export function renderRules(rules, state, profile, ctx = DEFAULT_CONTEXT) {
171
171
  for (const name of SECTION_ORDER)
172
172
  sections.set(name, []);
173
173
  for (const rule of hardRules) {
174
- sections.get('Must Not').push(ruleToText(rule));
174
+ sections.get('Must Not')?.push(ruleToText(rule));
175
175
  }
176
176
  for (const rule of otherRules) {
177
177
  const section = CATEGORY_TO_SECTION[rule.category] ?? 'Working Defaults';
178
- sections.get(section).push(ruleToText(rule));
178
+ sections.get(section)?.push(ruleToText(rule));
179
179
  }
180
180
  // 5. trust policy + pack 기본 규칙 주입
181
181
  if (ctx.include_pack_summary) {
182
- sections.get('Working Defaults').unshift(`Trust: ${trustPolicySummary(state.effective_trust_policy)}`);
182
+ sections.get('Working Defaults')?.unshift(`Trust: ${trustPolicySummary(state.effective_trust_policy)}`);
183
183
  // judgment pack 기본 규칙
184
184
  for (const rule of judgmentPackRules(state.judgment_pack)) {
185
- sections.get('Working Defaults').push(rule);
185
+ sections.get('Working Defaults')?.push(rule);
186
186
  }
187
187
  // communication pack 기본 규칙
188
188
  for (const rule of communicationPackRules(state.communication_pack)) {
189
- sections.get('How To Report').push(rule);
189
+ sections.get('How To Report')?.push(rule);
190
190
  }
191
191
  // 4축 facet 극단값 → 추가 규칙 (12-bucket pack 위에 연속 값 차별화).
192
192
  // 각 facet 0.5 default 이고 자동 갱신 ±0.1 단위이므로, 0.85/0.15 임계값은
193
193
  // 여러 세션에 걸친 강한 신호 누적 후에만 발화한다.
194
194
  for (const fr of facetDrivenRules(profile)) {
195
- sections.get(fr.section).push(fr.rule);
195
+ sections.get(fr.section)?.push(fr.rule);
196
196
  }
197
197
  }
198
198
  // 6. 섹션 조립 (AI-optimized: 간결한 태그 형식)
@@ -200,7 +200,7 @@ export function renderRules(rules, state, profile, ctx = DEFAULT_CONTEXT) {
200
200
  let totalChars = 0;
201
201
  let totalRules = 0;
202
202
  for (const name of SECTION_ORDER) {
203
- const items = sections.get(name);
203
+ const items = sections.get(name) ?? [];
204
204
  if (items.length === 0)
205
205
  continue;
206
206
  const header = `## ${name}`;
@@ -27,7 +27,7 @@ export function recordUsage(name, via = 'mcp') {
27
27
  try {
28
28
  fs.mkdirSync(STATE_DIR, { recursive: true });
29
29
  const entry = { at: new Date().toISOString(), name, via };
30
- fs.appendFileSync(COMPOUND_USAGE_LOG, JSON.stringify(entry) + '\n');
30
+ fs.appendFileSync(COMPOUND_USAGE_LOG, `${JSON.stringify(entry)}\n`);
31
31
  }
32
32
  catch {
33
33
  // fail-open: 신호 수집 실패가 사용자 경험을 방해하면 안 됨
@@ -67,7 +67,7 @@ export function appendImplicitFeedback(entry) {
67
67
  return false;
68
68
  try {
69
69
  fs.mkdirSync(STATE_DIR, { recursive: true });
70
- fs.appendFileSync(IMPLICIT_FEEDBACK_LOG, JSON.stringify(normalized) + '\n');
70
+ fs.appendFileSync(IMPLICIT_FEEDBACK_LOG, `${JSON.stringify(normalized)}\n`);
71
71
  return true;
72
72
  }
73
73
  catch {
@@ -147,7 +147,7 @@ export function migrateImplicitFeedbackLog() {
147
147
  }
148
148
  // atomic replace via temp file
149
149
  const tmp = `${IMPLICIT_FEEDBACK_LOG}.migrate.${process.pid}`;
150
- fs.writeFileSync(tmp, out.length > 0 ? out.join('\n') + '\n' : '');
150
+ fs.writeFileSync(tmp, out.length > 0 ? `${out.join('\n')}\n` : '');
151
151
  fs.renameSync(tmp, IMPLICIT_FEEDBACK_LOG);
152
152
  return { migrated, dropped };
153
153
  }
@@ -18,6 +18,17 @@ export declare function saveProfile(profile: Profile): void;
18
18
  * whether to run `runLegacyCutover`).
19
19
  */
20
20
  export declare function profileExists(): boolean;
21
+ /**
22
+ * profile.json 이 존재하지만 parse 실패 / v1 shape 위반인 경우, 사용자가
23
+ * 다음 onboarding 으로 매끄럽게 복구되도록 corrupt 파일을 timestamp 백업
24
+ * 으로 옆에 치워둔다. 백업 경로를 반환.
25
+ *
26
+ * v0.4.8 — bootstrapV1Session 의 loadProfile()=null early-return 보강.
27
+ * 이전엔 needsOnboarding=true 만 반환했고 corrupt 파일이 그대로 남아
28
+ * 다음 실행 때도 동일 분기로 빠지면서 사용자가 정체 원인을 모른 채
29
+ * onboarding 안내만 반복 받는 패턴이었음.
30
+ */
31
+ export declare function backupCorruptProfile(): string | null;
21
32
  export declare function isV1Profile(data: unknown): data is Profile;
22
33
  /**
23
34
  * D2 fix (2026-04-27): explicit_correction 누적 시 해당 축의 confidence 를 점진
@@ -66,6 +66,29 @@ export function saveProfile(profile) {
66
66
  export function profileExists() {
67
67
  return fs.existsSync(FORGE_PROFILE);
68
68
  }
69
+ /**
70
+ * profile.json 이 존재하지만 parse 실패 / v1 shape 위반인 경우, 사용자가
71
+ * 다음 onboarding 으로 매끄럽게 복구되도록 corrupt 파일을 timestamp 백업
72
+ * 으로 옆에 치워둔다. 백업 경로를 반환.
73
+ *
74
+ * v0.4.8 — bootstrapV1Session 의 loadProfile()=null early-return 보강.
75
+ * 이전엔 needsOnboarding=true 만 반환했고 corrupt 파일이 그대로 남아
76
+ * 다음 실행 때도 동일 분기로 빠지면서 사용자가 정체 원인을 모른 채
77
+ * onboarding 안내만 반복 받는 패턴이었음.
78
+ */
79
+ export function backupCorruptProfile() {
80
+ if (!fs.existsSync(FORGE_PROFILE))
81
+ return null;
82
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
83
+ const backupPath = `${FORGE_PROFILE}.corrupt-${ts}`;
84
+ try {
85
+ fs.renameSync(FORGE_PROFILE, backupPath);
86
+ return backupPath;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
69
92
  export function isV1Profile(data) {
70
93
  if (!data || typeof data !== 'object')
71
94
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooojin/forgen",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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.6",
4
+ "version": "0.4.8",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",