@wooojin/forgen 0.4.13 → 0.5.0

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 (139) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +94 -0
  3. package/README.ja.md +4 -4
  4. package/README.md +89 -27
  5. package/README.zh.md +4 -4
  6. package/assets/claude/commands/forge-loop.md +7 -1
  7. package/assets/claude/commands/ship.md +18 -1
  8. package/assets/opencode/forgen.ts +52 -0
  9. package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
  10. package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
  11. package/dist/checks/_shared/model-profile.d.ts +25 -0
  12. package/dist/checks/_shared/model-profile.js +63 -0
  13. package/dist/cli.js +99 -153
  14. package/dist/core/auto-compound-runner.d.ts +0 -11
  15. package/dist/core/auto-compound-runner.js +127 -79
  16. package/dist/core/compound-consent.d.ts +15 -0
  17. package/dist/core/compound-consent.js +48 -0
  18. package/dist/core/compound-sweep-cli.d.ts +57 -0
  19. package/dist/core/compound-sweep-cli.js +354 -0
  20. package/dist/core/config-injector.d.ts +16 -1
  21. package/dist/core/config-injector.js +47 -28
  22. package/dist/core/dashboard-cli.js +40 -16
  23. package/dist/core/dashboard.d.ts +3 -4
  24. package/dist/core/dashboard.js +15 -34
  25. package/dist/core/dev-cli.d.ts +13 -0
  26. package/dist/core/dev-cli.js +70 -0
  27. package/dist/core/doctor.d.ts +16 -5
  28. package/dist/core/doctor.js +161 -176
  29. package/dist/core/drift-score.d.ts +2 -0
  30. package/dist/core/drift-score.js +9 -1
  31. package/dist/core/harness.js +115 -43
  32. package/dist/core/health-cli.d.ts +2 -0
  33. package/dist/core/health-cli.js +6 -1
  34. package/dist/core/host-detect.d.ts +3 -1
  35. package/dist/core/host-detect.js +26 -1
  36. package/dist/core/migrate-cli.js +15 -0
  37. package/dist/core/migrate-evidence-host.d.ts +2 -1
  38. package/dist/core/migrate-tenetx.d.ts +50 -0
  39. package/dist/core/migrate-tenetx.js +262 -0
  40. package/dist/core/probe-workflow-cli.d.ts +3 -3
  41. package/dist/core/probe-workflow-cli.js +13 -13
  42. package/dist/core/recall-cli.js +1 -1
  43. package/dist/core/regress-map-cli.js +1 -1
  44. package/dist/core/rendered-rules-manifest.d.ts +29 -0
  45. package/dist/core/rendered-rules-manifest.js +60 -0
  46. package/dist/core/session-store.js +14 -3
  47. package/dist/core/settings-injector.d.ts +3 -0
  48. package/dist/core/settings-injector.js +12 -16
  49. package/dist/core/spawn.d.ts +11 -1
  50. package/dist/core/spawn.js +79 -7
  51. package/dist/core/state-gc.js +1 -0
  52. package/dist/core/status-cli.d.ts +20 -0
  53. package/dist/core/status-cli.js +100 -0
  54. package/dist/core/statusline-cli.d.ts +7 -0
  55. package/dist/core/statusline-cli.js +63 -19
  56. package/dist/core/transcript-summary.d.ts +18 -0
  57. package/dist/core/transcript-summary.js +81 -0
  58. package/dist/core/trust-layer-intent.d.ts +21 -1
  59. package/dist/core/trust-layer-intent.js +7 -0
  60. package/dist/core/types.d.ts +3 -2
  61. package/dist/core/uninstall.js +12 -0
  62. package/dist/core/usage-telemetry.d.ts +7 -1
  63. package/dist/core/usage-telemetry.js +7 -9
  64. package/dist/core/v1-bootstrap.js +1 -1
  65. package/dist/core/watch-cli.js +1 -1
  66. package/dist/engine/compound-extractor.js +10 -0
  67. package/dist/engine/compound-loop.js +35 -6
  68. package/dist/engine/compound-share.d.ts +85 -0
  69. package/dist/engine/compound-share.js +606 -0
  70. package/dist/engine/correction-cluster-runner.d.ts +38 -0
  71. package/dist/engine/correction-cluster-runner.js +188 -0
  72. package/dist/engine/correction-clustering.d.ts +80 -0
  73. package/dist/engine/correction-clustering.js +167 -0
  74. package/dist/engine/enforce-classifier.d.ts +10 -1
  75. package/dist/engine/enforce-classifier.js +111 -22
  76. package/dist/engine/extraction-session.js +10 -3
  77. package/dist/engine/private-filter.d.ts +36 -0
  78. package/dist/engine/private-filter.js +100 -0
  79. package/dist/engine/ranking-pipeline.js +4 -2
  80. package/dist/engine/relevance-gate.d.ts +12 -0
  81. package/dist/engine/relevance-gate.js +12 -0
  82. package/dist/engine/roi-demotion.d.ts +79 -0
  83. package/dist/engine/roi-demotion.js +159 -0
  84. package/dist/engine/solution-format.d.ts +1 -0
  85. package/dist/engine/solution-format.js +26 -0
  86. package/dist/engine/solution-matcher.js +10 -1
  87. package/dist/fgx.js +7 -6
  88. package/dist/forge/cli.js +8 -2
  89. package/dist/hooks/compound-reflection.js +6 -1
  90. package/dist/hooks/context-guard.d.ts +16 -1
  91. package/dist/hooks/context-guard.js +92 -46
  92. package/dist/hooks/post-tool-use.js +2 -3
  93. package/dist/hooks/pre-compact.js +14 -0
  94. package/dist/hooks/pre-tool-use.js +5 -1
  95. package/dist/hooks/shared/stop-triggers.d.ts +29 -2
  96. package/dist/hooks/shared/stop-triggers.js +35 -2
  97. package/dist/hooks/solution-injector.d.ts +28 -0
  98. package/dist/hooks/solution-injector.js +126 -28
  99. package/dist/hooks/stop-guard.js +5 -2
  100. package/dist/hooks/subagent-stop-guard.js +4 -1
  101. package/dist/host/capabilities-claude.js +1 -0
  102. package/dist/host/capabilities-codex.js +1 -0
  103. package/dist/host/capabilities-opencode.d.ts +26 -0
  104. package/dist/host/capabilities-opencode.js +78 -0
  105. package/dist/host/capabilities-registry.d.ts +7 -0
  106. package/dist/host/capabilities-registry.js +14 -0
  107. package/dist/host/exec-host.d.ts +4 -3
  108. package/dist/host/exec-host.js +2 -0
  109. package/dist/host/host-binding.d.ts +27 -0
  110. package/dist/host/host-binding.js +11 -0
  111. package/dist/host/host-runtime.js +18 -0
  112. package/dist/host/install-codex.d.ts +8 -0
  113. package/dist/host/install-codex.js +2 -2
  114. package/dist/host/install-opencode.d.ts +38 -0
  115. package/dist/host/install-opencode.js +148 -0
  116. package/dist/host/install-orchestrator.d.ts +4 -1
  117. package/dist/host/install-orchestrator.js +20 -1
  118. package/dist/host/invoke-agent.d.ts +3 -2
  119. package/dist/host/opencode/context-cli.d.ts +15 -0
  120. package/dist/host/opencode/context-cli.js +25 -0
  121. package/dist/host/opencode/guard-cli.d.ts +13 -0
  122. package/dist/host/opencode/guard-cli.js +39 -0
  123. package/dist/host/opencode/plugin/forgen.d.ts +31 -0
  124. package/dist/host/opencode/plugin/forgen.js +60 -0
  125. package/dist/host/opencode/translate.d.ts +49 -0
  126. package/dist/host/opencode/translate.js +96 -0
  127. package/dist/host/parity-harness.d.ts +10 -2
  128. package/dist/host/projection.js +11 -0
  129. package/dist/mcp/tools.js +17 -4
  130. package/dist/store/evidence-store.d.ts +2 -6
  131. package/dist/store/evidence-store.js +68 -14
  132. package/dist/store/host-mismatch.d.ts +2 -1
  133. package/dist/store/host-mismatch.js +8 -8
  134. package/dist/store/profile-store.d.ts +3 -2
  135. package/dist/store/types.d.ts +9 -2
  136. package/package.json +3 -2
  137. package/plugin.json +2 -2
  138. package/skills/forge-loop/SKILL.md +7 -1
  139. package/skills/ship/SKILL.md +18 -1
@@ -20,6 +20,8 @@ export interface DriftState {
20
20
  lastWarningAt: number;
21
21
  lastCriticalAt: number;
22
22
  hardCapReached: boolean;
23
+ /** 최근 hardcap 발화 timestamp (쿨다운용, ADR-013). */
24
+ lastHardcapAt?: number;
23
25
  }
24
26
  export interface DriftResult {
25
27
  level: 'normal' | 'warning' | 'critical' | 'hardcap';
@@ -54,9 +54,17 @@ export function evaluateDrift(state, isEdit, isRevert, thresholds = {}) {
54
54
  // Calculate drift score: edit rate 65% + revert rate 35%
55
55
  const rawScore = (state.ewmaEditRate * 65) + (state.ewmaRevertRate * 35);
56
56
  const score = Math.min(100, Math.max(0, Math.round(rawScore)));
57
- // Hard cap
57
+ // Hard cap — ADR-013 버그수정: 이전엔 cooldown 이 없어 50편집 넘으면 **매 편집마다**
58
+ // hardcap(=drift_critical) 을 발화 → drift_critical 이 세션길이 카운터로 전락(2067/2100
59
+ // 이벤트가 score100). critical 처럼 cooldown 게이트를 둬 반복 발화를 억제. 상태
60
+ // (hardCapReached)는 항상 갱신하되, message 는 쿨다운 밖에서만 낸다(null=emitter skip).
58
61
  if (state.totalEdits >= t.hardCapEdits) {
59
62
  state.hardCapReached = true;
63
+ const withinCooldown = now - (state.lastHardcapAt ?? 0) <= t.criticalCooldownMs;
64
+ if (withinCooldown) {
65
+ return { level: 'hardcap', score: 100, message: null };
66
+ }
67
+ state.lastHardcapAt = now;
60
68
  return {
61
69
  level: 'hardcap',
62
70
  score: 100,
@@ -17,10 +17,13 @@ import { buildEnv, generateClaudeRuleFiles, registerTmuxBindings } from './confi
17
17
  import { createLogger } from './logger.js';
18
18
  import { HANDOFFS_DIR, ME_BEHAVIOR, ME_DIR, ME_RULES, ME_SKILLS, ME_SOLUTIONS, SESSIONS_DIR, STATE_DIR, FORGEN_HOME } from './paths.js';
19
19
  import { RULE_FILE_CAPS } from '../hooks/shared/injection-caps.js';
20
+ import { recordRenderedFiles } from './rendered-rules-manifest.js';
20
21
  import { rollbackSettings, } from './settings-lock.js';
21
22
  import { bootstrapV1Session, ensureV1Directories } from './v1-bootstrap.js';
22
23
  import { injectSettings } from './settings-injector.js';
23
24
  import { installAgents, installSlashCommands } from './installer.js';
25
+ import { getHostCapabilities } from '../host/capabilities-registry.js';
26
+ import { getProjection } from '../host/projection.js';
24
27
  const log = createLogger('harness');
25
28
  /**
26
29
  * 0.4.6 — Codex hooks fast staleness check + auto-reconcile.
@@ -108,11 +111,10 @@ export { rollbackSettings };
108
111
  function injectClaudeRuleFiles(cwd, ruleFiles) {
109
112
  const PER_RULE_CAP = RULE_FILE_CAPS.perRuleFile;
110
113
  const TOTAL_CAP = RULE_FILE_CAPS.totalRuleFiles;
111
- const globalRulesDir = path.join(os.homedir(), '.claude', 'rules');
112
114
  const projectRulesDir = path.join(cwd, '.claude', 'rules');
113
- fs.mkdirSync(globalRulesDir, { recursive: true });
114
115
  fs.mkdirSync(projectRulesDir, { recursive: true });
115
116
  let totalWritten = 0;
117
+ const writtenForManifest = {};
116
118
  for (const [filename, content] of Object.entries(ruleFiles)) {
117
119
  const capped = content.length > PER_RULE_CAP
118
120
  ? `${content.slice(0, PER_RULE_CAP)}\n... (capped at rule file limit)\n`
@@ -121,11 +123,35 @@ function injectClaudeRuleFiles(cwd, ruleFiles) {
121
123
  log.debug(`rules/ 총량 캡 도달, ${filename} 생략`);
122
124
  break;
123
125
  }
124
- const isUserPreference = filename.startsWith('forge-');
125
- const targetDir = isUserPreference ? globalRulesDir : projectRulesDir;
126
- fs.writeFileSync(path.join(targetDir, filename), capped);
126
+ // ADR-010 W1-3 (F6 수정): 모든 규칙 파일은 프로젝트 스코프로만 주입한다.
127
+ // 이전에는 `forge-*` 파일명이 글로벌 ~/.claude/rules/ 로 라우팅되어
128
+ // 프로젝트 맥락의 behavioral 패턴이 전 프로젝트 세션에 주입됐다.
129
+ // 목표 상태: 글로벌 rules 디렉토리에 forgen 산출물 0개.
130
+ const target = path.join(projectRulesDir, filename);
131
+ fs.writeFileSync(target, capped);
132
+ writtenForManifest[target] = capped;
127
133
  totalWritten += capped.length;
128
134
  }
135
+ // ADR-010 W1-1: 쓴 그대로의 content-hash 를 manifest 에 기록 —
136
+ // reclaimer 가 "forgen 산출물 그대로"와 "사용자 편집본"을 구분하는 근거.
137
+ try {
138
+ const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
139
+ recordRenderedFiles(writtenForManifest, String(pkg.version ?? '0.0.0'));
140
+ }
141
+ catch (e) {
142
+ log.debug('rendered-rules manifest 기록 실패 (fail-open)', e);
143
+ }
144
+ // 마이그레이션 (W1-3): 구버전이 글로벌에 쓴 forge-behavioral.md 회수.
145
+ // (tenetx 계열 글로벌 잔재는 W1-1 reclaimer 의 영역)
146
+ const legacyGlobalBehavioral = path.join(os.homedir(), '.claude', 'rules', 'forge-behavioral.md');
147
+ if (fs.existsSync(legacyGlobalBehavioral)) {
148
+ try {
149
+ fs.unlinkSync(legacyGlobalBehavioral);
150
+ }
151
+ catch (e) {
152
+ log.debug('레거시 글로벌 behavioral 삭제 실패', e);
153
+ }
154
+ }
129
155
  // 마이그레이션: 이전 위치 파일 제거
130
156
  const legacyPath = path.join(cwd, '.claude', 'compound-rules.md');
131
157
  if (fs.existsSync(legacyPath)) {
@@ -333,6 +359,81 @@ function checkCompoundStaleness() {
333
359
  log.debug('Staleness check failed (non-fatal)', e);
334
360
  }
335
361
  }
362
+ // ── Host Bindings (Multi-Harness Adapter Plan P0 — 구 if-ladder 대체) ──
363
+ /**
364
+ * Claude 세션 준비: settings.json 인젝션 + 에이전트 설치 + 규칙 파일 생성/주입 +
365
+ * 슬래시 명령 설치. (구 `if (runtime === 'claude')` 분기, 동작 동일.)
366
+ */
367
+ async function prepareClaudeSession(ctx) {
368
+ const { cwd, pkgRoot, env, v1Result } = ctx;
369
+ try {
370
+ injectSettings(env, v1Result, 'claude', cwd, pkgRoot);
371
+ }
372
+ catch (e) {
373
+ const msg = e instanceof Error ? e.message : String(e);
374
+ if (msg.includes('settings.json lock') || msg.includes('SettingsLockError')) {
375
+ console.error(`[forgen] ${msg} — settings 갱신 스킵, 이전 값 유지`);
376
+ }
377
+ else {
378
+ throw e;
379
+ }
380
+ }
381
+ installAgents(cwd, pkgRoot);
382
+ const ruleFiles = generateClaudeRuleFiles(cwd, v1Result.renderedRules);
383
+ injectClaudeRuleFiles(cwd, ruleFiles);
384
+ installSlashCommands(cwd, pkgRoot);
385
+ }
386
+ /**
387
+ * Codex 세션 준비: hooks.json 자동 reconcile-on-launch.
388
+ * (구 `else if (runtime === 'codex')` 분기, 동작 동일.)
389
+ */
390
+ async function prepareCodexSession(ctx) {
391
+ try {
392
+ await ensureCodexHooksFresh(ctx.pkgRoot);
393
+ }
394
+ catch (e) {
395
+ log.debug('Codex hooks reconcile 실패 (fail-open)', e);
396
+ }
397
+ }
398
+ /**
399
+ * OpenCode 세션 준비. OpenCode 는 in-process plugin 형태라, 세션 준비 시점의 host-specific
400
+ * artifact wiring 이 없다 — plugin 배포는 `forgen install opencode`(install-opencode)가
401
+ * 수행하고, OpenCode 가 세션마다 plugin 을 자동 로드한다. 따라서 여기선 no-op.
402
+ * (subprocess projection 은 OpenCode 의 plugin 모델과 무관 — projection.ts 스텁 유지.)
403
+ */
404
+ async function prepareOpencodeSession(_ctx) {
405
+ // no-op: plugin 은 install-opencode 가 배포, OpenCode 가 자동 로드.
406
+ }
407
+ /**
408
+ * capabilities 선언 + projection 함수 + 세션 준비(hook-surface wiring)를 host 별로
409
+ * 묶은 registry. `Record<HostId, HostBinding>` 이 완전성을 강제하므로 새 host 추가 시
410
+ * (예: OpenCode, P1) 엔트리 누락은 컴파일 타임에 걸린다.
411
+ *
412
+ * capabilities/projection 은 현재 prepareHarness 자체가 소비하지 않지만(각각
413
+ * capabilities-registry.ts / parity-harness.ts 가 독립 소비), HostBinding 계약의
414
+ * 일부로 함께 선언해 두어 향후 확장(§4.1 P1) 시 registry 엔트리 하나만 추가하면
415
+ * 되는 형태를 만든다.
416
+ */
417
+ const HOST_BINDINGS = {
418
+ claude: {
419
+ id: 'claude',
420
+ capabilities: getHostCapabilities('claude'),
421
+ projection: getProjection('claude'),
422
+ prepareSession: prepareClaudeSession,
423
+ },
424
+ codex: {
425
+ id: 'codex',
426
+ capabilities: getHostCapabilities('codex'),
427
+ projection: getProjection('codex'),
428
+ prepareSession: prepareCodexSession,
429
+ },
430
+ opencode: {
431
+ id: 'opencode',
432
+ capabilities: getHostCapabilities('opencode'),
433
+ projection: getProjection('opencode'),
434
+ prepareSession: prepareOpencodeSession,
435
+ },
436
+ };
336
437
  export async function prepareHarness(cwd, options = {}) {
337
438
  const runtime = options.runtime ?? 'claude';
338
439
  try {
@@ -343,7 +444,7 @@ export async function prepareHarness(cwd, options = {}) {
343
444
  // 2. v1 Session Bootstrap (legacy 감지 → profile 로드 → preset 합성 → rule 렌더)
344
445
  const v1Result = bootstrapV1Session();
345
446
  if (v1Result.needsOnboarding) {
346
- log.debug('v1: 온보딩 필요 — forgen onboarding 실행 안내');
447
+ log.debug('v1: 온보딩 필요 — forgen forge --onboarding 실행 안내');
347
448
  }
348
449
  if (v1Result.legacyBackupPath) {
349
450
  log.debug(`v1: 레거시 프로필 백업 완료 → ${v1Result.legacyBackupPath}`);
@@ -378,48 +479,19 @@ export async function prepareHarness(cwd, options = {}) {
378
479
  }
379
480
  // 3. 환경 확인
380
481
  const inTmux = !!process.env.TMUX;
381
- // 4-7. Claude artifact 작업 (settings.json + agents + rules + slash commands).
482
+ // 4-7. Host artifact 작업 (Claude: settings.json + agents + rules + slash commands.
483
+ // Codex: hooks.json reconcile). HOST_BINDINGS 로 dispatch — host 별 구체 동작은
484
+ // prepareClaudeSession / prepareCodexSession 참조.
382
485
  //
383
486
  // feat/codex-support P1-7 (2026-04-27): runtime === 'codex' 시 *.claude/* 계열
384
487
  // 작업은 *no-op*. Codex 측 동치 prep 은 Phase 3 (install-codex.ts 의 prompts +
385
- // AGENTS.md inject) 에서 처리. 본 분기는 *Claude artifact 가 Codex 환경을
386
- // 오염시키지 않도록* 보호하는 비대칭 게이트.
488
+ // AGENTS.md inject) 에서 처리. 본 dispatch 는 *Claude artifact 가 Codex 환경을
489
+ // 오염시키지 않도록* 보호하는 비대칭 게이트를 그대로 보존한다.
387
490
  const pkgRoot = getPackageRoot();
388
491
  const env = buildEnv(cwd, v1Result.session?.session_id, runtime);
389
- if (runtime === 'claude') {
390
- // 4. settings.json 인젝션
391
- try {
392
- injectSettings(env, v1Result, runtime, cwd, pkgRoot);
393
- }
394
- catch (e) {
395
- const msg = e instanceof Error ? e.message : String(e);
396
- if (msg.includes('settings.json lock') || msg.includes('SettingsLockError')) {
397
- console.error(`[forgen] ${msg} — settings 갱신 스킵, 이전 값 유지`);
398
- }
399
- else {
400
- throw e;
401
- }
402
- }
403
- // 5. 에이전트 설치
404
- installAgents(cwd, pkgRoot);
405
- // 6. 규칙 파일 생성 + 주입
406
- const ruleFiles = generateClaudeRuleFiles(cwd, v1Result.renderedRules);
407
- injectClaudeRuleFiles(cwd, ruleFiles);
408
- // 7. 슬래시 명령 설치
409
- installSlashCommands(cwd, pkgRoot);
410
- }
411
- else if (runtime === 'codex') {
412
- // 0.4.6 — Codex hooks 자동 reconcile-on-launch.
413
- // 매 fgx --codex / forgen --runtime codex 호출 시 hooks.json 의 forgen entry 가
414
- // 현재 pkgRoot 와 일치하는지 fast check. 불일치 (다른 머신/버전 install,
415
- // pkg path 변경, 신규 설치) → silent reinstall.
416
- // 사용자가 `forgen install codex` 를 명시 호출 안 해도 매번 정합성 보장.
417
- try {
418
- await ensureCodexHooksFresh(pkgRoot);
419
- }
420
- catch (e) {
421
- log.debug('Codex hooks reconcile 실패 (fail-open)', e);
422
- }
492
+ const binding = HOST_BINDINGS[runtime];
493
+ if (binding) {
494
+ await binding.prepareSession({ cwd, pkgRoot, env, v1Result });
423
495
  }
424
496
  else {
425
497
  log.debug(`prepareHarness: runtime=${runtime} — artifact prep skipped`);
@@ -20,4 +20,6 @@ export interface HealthScore {
20
20
  grade: 'A' | 'B' | 'C' | 'D' | 'F';
21
21
  }
22
22
  export declare function computeHealth(): HealthScore;
23
+ /** 한 줄 health 요약 — `forgen status` 요약 헤더에서 재사용. */
24
+ export declare function renderHealthLine(h: HealthScore): string;
23
25
  export declare function handleHealth(): Promise<void>;
@@ -71,11 +71,16 @@ function bar(value, max, width = 10) {
71
71
  const filled = Math.round((value / max) * width);
72
72
  return '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, width - filled));
73
73
  }
74
+ /** 한 줄 health 요약 — `forgen status` 요약 헤더에서 재사용. */
75
+ export function renderHealthLine(h) {
76
+ const gc = gradeColor(h.grade);
77
+ return `\n ${C.bold}forgen status${C.reset} ${gc}${C.bold}${h.grade}${C.reset} ${gc}${h.total}/100${C.reset}`;
78
+ }
74
79
  export async function handleHealth() {
75
80
  const h = computeHealth();
76
81
  const gc = gradeColor(h.grade);
77
82
  console.log('');
78
- console.log(` ${C.bold}forgen health${C.reset} ${gc}${C.bold}${h.grade}${C.reset} ${gc}${h.total}/100${C.reset}`);
83
+ console.log(` ${C.bold}forgen status${C.reset} ${gc}${C.bold}${h.grade}${C.reset} ${gc}${h.total}/100${C.reset}`);
79
84
  console.log('');
80
85
  console.log(` Utilization ${bar(h.components.utilization, 30)} ${h.components.utilization}/30 ${C.dim}solution match rate (7d)${C.reset}`);
81
86
  console.log(` Effectiveness ${bar(h.components.effectiveness, 25)} ${h.components.effectiveness}/25 ${C.dim}block→ack ratio${C.reset}`);
@@ -34,7 +34,9 @@ export interface HostAvailability {
34
34
  export interface HostDetectionResult {
35
35
  readonly claude: HostAvailability;
36
36
  readonly codex: HostAvailability;
37
- /** 둘 다 사용 가능. */
37
+ /** OpenCode (W3-3 P1) — 감지 전용. 설치는 plugin 슬림 착지 후. */
38
+ readonly opencode: HostAvailability;
39
+ /** claude+codex 둘 다 사용 가능 (기존 primary-pair 계약). */
38
40
  readonly bothAvailable: boolean;
39
41
  /** 하나도 사용 가능하지 않음 (warn). */
40
42
  readonly noneAvailable: boolean;
@@ -56,13 +56,38 @@ function detectCodex() {
56
56
  available: binaryFound || homeExists,
57
57
  };
58
58
  }
59
+ /**
60
+ * OpenCode 감지 (W3-3 P1 파운데이션). OpenCode 는 XDG 관습을 따라 `~/.config/opencode/`
61
+ * 를 쓴다(`~/.opencode` 아님). binary 는 `opencode`. auth 패턴은 별도 확인 안 함.
62
+ * 감지만 — 설치(plugin 슬림)는 P1 후속이라 install-orchestrator 에서 "detected, install pending"
63
+ * 로만 표기한다.
64
+ */
65
+ function detectOpencode() {
66
+ const binaryPath = which('opencode');
67
+ const homePath = path.join(os.homedir(), '.config', 'opencode');
68
+ const homeExists = fs.existsSync(homePath);
69
+ const binaryFound = binaryPath !== null;
70
+ return {
71
+ host: 'opencode',
72
+ binaryFound,
73
+ binaryPath,
74
+ homeExists,
75
+ homePath,
76
+ authPresent: null,
77
+ available: binaryFound || homeExists,
78
+ };
79
+ }
59
80
  export function detectAvailableHosts() {
60
81
  const claude = detectClaude();
61
82
  const codex = detectCodex();
83
+ const opencode = detectOpencode();
62
84
  return {
63
85
  claude,
64
86
  codex,
87
+ opencode,
88
+ // bothAvailable 는 기존 계약(claude+codex primary pair) 유지. noneAvailable 은
89
+ // opencode 까지 포함해 "어떤 host 도 없음" 을 정확히 반영.
65
90
  bothAvailable: claude.available && codex.available,
66
- noneAvailable: !claude.available && !codex.available,
91
+ noneAvailable: !claude.available && !codex.available && !opencode.available,
67
92
  };
68
93
  }
@@ -19,6 +19,10 @@ const HELP = `
19
19
  forgen migrate evidence-host behavior/*.json 에 host 필드 백필
20
20
  --dry-run 디스크 미수정, 카운트만 출력
21
21
  --default-host <claude|codex> host 기본값 (default: claude)
22
+ forgen migrate tenetx tenetx/legacy 규칙 스프롤 회수 (ADR-010 W1-1)
23
+ --dry-run 회수 대상만 표시
24
+ --yes marker-only(사용자 편집 가능성) 파일도 회수
25
+ --apply-settings settings.json/registry 의 tenetx 항목 제거
22
26
  forgen migrate --help 이 도움말
23
27
  `;
24
28
  export async function handleMigrate(args) {
@@ -47,6 +51,17 @@ export async function handleMigrate(args) {
47
51
  console.log(`[forgen] migrated: ${result.migrated} (skipped: ${result.skipped}, total: ${result.total})${label}`);
48
52
  return;
49
53
  }
54
+ if (sub === 'tenetx') {
55
+ const { runReclaim, printReclaimResult } = await import('./migrate-tenetx.js');
56
+ const result = runReclaim({
57
+ cwd: process.cwd(),
58
+ dryRun: args.includes('--dry-run'),
59
+ yes: args.includes('--yes'),
60
+ applySettings: args.includes('--apply-settings'),
61
+ });
62
+ printReclaimResult(result);
63
+ return;
64
+ }
50
65
  console.error(`[forgen migrate] unknown target: ${sub}`);
51
66
  console.error(HELP);
52
67
  process.exit(1);
@@ -8,9 +8,10 @@
8
8
  * loadEvidence 의 자동 backfill (evidence-store.ts::backfillHost) 과는 독립.
9
9
  * 이 모듈은 디스크 파일을 직접 수정하는 명시적 경로.
10
10
  */
11
+ import type { HostId } from './trust-layer-intent.js';
11
12
  export interface MigrateEvidenceHostOptions {
12
13
  /** backfill 할 host 값 (host 필드 없는 파일에만 적용) */
13
- defaultHost: 'claude' | 'codex';
14
+ defaultHost: HostId;
14
15
  /**
15
16
  * true 이면 디스크를 수정하지 않고 카운트만 반환.
16
17
  * @default false
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Forgen v0.5.0 — `forgen migrate tenetx` (ADR-010 W1-1, Rule Reclaimer)
3
+ *
4
+ * tenetx(forgen 의 레거시 정체성) 및 구버전 forgen 이 ~/.claude/rules/ 와
5
+ * <cwd>/.claude/rules/ 에 남긴 규칙 파일 스프롤을 provenance 기반으로 회수한다.
6
+ * 2026-07-16 수동 청소(56K→0, 중복 스킬 21개 제거)의 프로덕트화.
7
+ *
8
+ * 판정 3단계 (설계: docs/plans/2026-07-16-v0.5.0-execution-plan.md W1-1):
9
+ * (a) content-hash 가 rendered-rules-manifest 와 일치 → 무프롬프트 삭제 (forgen 이 쓴 그대로)
10
+ * (b) provenance 마커만 매치 + hash 불일치 → --yes 필요 (사용자 편집 가능성)
11
+ * (c) 매치 없음 → 불간섭
12
+ *
13
+ * 안전장치: 모든 삭제는 백업 이동(가역), --dry-run 지원,
14
+ * settings.json/installed_plugins.json 편집은 --apply-settings 명시 동의 시에만.
15
+ */
16
+ export type ReclaimReason = 'manifest-hash' | 'provenance-marker';
17
+ export interface ReclaimCandidate {
18
+ path: string;
19
+ reason: ReclaimReason;
20
+ }
21
+ export interface ReclaimScan {
22
+ /** (a) 무프롬프트 삭제 가능 — forgen 이 쓴 그대로 */
23
+ removable: ReclaimCandidate[];
24
+ /** (b) 마커는 있으나 hash 불일치 — --yes 필요 */
25
+ needsConfirm: ReclaimCandidate[];
26
+ /** settings/registry 의 tenetx 흔적 (사람이 읽는 설명) */
27
+ settingsFindings: string[];
28
+ }
29
+ export interface ReclaimOptions {
30
+ cwd: string;
31
+ home?: string;
32
+ dryRun?: boolean;
33
+ /** marker-only(b) 항목도 삭제에 포함 */
34
+ yes?: boolean;
35
+ /** settings.json / installed_plugins.json 에서 tenetx 항목 직접 제거 */
36
+ applySettings?: boolean;
37
+ }
38
+ export interface ReclaimResult {
39
+ removed: string[];
40
+ skippedNeedsConfirm: string[];
41
+ settingsFindings: string[];
42
+ settingsApplied: boolean;
43
+ backupDir: string | null;
44
+ dryRun: boolean;
45
+ }
46
+ export declare function hasProvenanceMarker(content: string): boolean;
47
+ /** 스캔만 — 디스크 수정 없음. doctor --reclaim 이 그대로 사용. */
48
+ export declare function scanReclaimables(cwd: string, home?: string): ReclaimScan;
49
+ export declare function runReclaim(opts: ReclaimOptions): ReclaimResult;
50
+ export declare function printReclaimResult(r: ReclaimResult): void;