@wooojin/forgen 0.4.13 → 0.5.5
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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +261 -0
- package/README.ja.md +16 -12
- package/README.ko.md +12 -8
- package/README.md +90 -27
- package/README.zh.md +16 -12
- package/assets/claude/commands/forge-loop.md +7 -1
- package/assets/claude/commands/ship.md +18 -1
- package/assets/opencode/forgen.ts +52 -0
- package/assets/shared/hook-registry.json +210 -22
- package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
- package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
- package/dist/checks/_shared/model-profile.d.ts +25 -0
- package/dist/checks/_shared/model-profile.js +63 -0
- package/dist/cli.js +99 -153
- package/dist/core/auto-compound-runner.d.ts +0 -11
- package/dist/core/auto-compound-runner.js +199 -92
- package/dist/core/changelog-cli.d.ts +4 -1
- package/dist/core/changelog-cli.js +8 -7
- package/dist/core/compound-consent.d.ts +15 -0
- package/dist/core/compound-consent.js +48 -0
- package/dist/core/compound-sweep-cli.d.ts +60 -0
- package/dist/core/compound-sweep-cli.js +374 -0
- package/dist/core/config-injector.d.ts +16 -1
- package/dist/core/config-injector.js +47 -28
- package/dist/core/dashboard-cli.js +40 -16
- package/dist/core/dashboard.d.ts +3 -4
- package/dist/core/dashboard.js +15 -34
- package/dist/core/dev-cli.d.ts +13 -0
- package/dist/core/dev-cli.js +70 -0
- package/dist/core/doctor.d.ts +16 -5
- package/dist/core/doctor.js +190 -176
- package/dist/core/drift-score.d.ts +2 -0
- package/dist/core/drift-score.js +9 -1
- package/dist/core/harness.js +115 -43
- package/dist/core/health-cli.d.ts +2 -0
- package/dist/core/health-cli.js +6 -1
- package/dist/core/host-detect.d.ts +3 -1
- package/dist/core/host-detect.js +26 -1
- package/dist/core/migrate-cli.js +15 -0
- package/dist/core/migrate-evidence-host.d.ts +2 -1
- package/dist/core/migrate-tenetx.d.ts +50 -0
- package/dist/core/migrate-tenetx.js +262 -0
- package/dist/core/probe-workflow-cli.d.ts +3 -3
- package/dist/core/probe-workflow-cli.js +13 -13
- package/dist/core/recall-cli.js +1 -1
- package/dist/core/regress-map-cli.js +1 -1
- package/dist/core/rendered-rules-manifest.d.ts +29 -0
- package/dist/core/rendered-rules-manifest.js +60 -0
- package/dist/core/session-store.js +14 -3
- package/dist/core/settings-injector.d.ts +3 -0
- package/dist/core/settings-injector.js +12 -16
- package/dist/core/spawn.d.ts +37 -1
- package/dist/core/spawn.js +116 -9
- package/dist/core/state-gc.js +1 -0
- package/dist/core/status-cli.d.ts +20 -0
- package/dist/core/status-cli.js +100 -0
- package/dist/core/statusline-cli.d.ts +7 -0
- package/dist/core/statusline-cli.js +63 -19
- package/dist/core/transcript-summary.d.ts +18 -0
- package/dist/core/transcript-summary.js +81 -0
- package/dist/core/trust-layer-intent.d.ts +21 -1
- package/dist/core/trust-layer-intent.js +7 -0
- package/dist/core/types.d.ts +3 -2
- package/dist/core/uninstall.js +12 -0
- package/dist/core/usage-telemetry.d.ts +7 -1
- package/dist/core/usage-telemetry.js +7 -9
- package/dist/core/v1-bootstrap.js +1 -1
- package/dist/core/watch-cli.js +1 -1
- package/dist/engine/compound-extractor.js +10 -0
- package/dist/engine/compound-loop.js +55 -7
- package/dist/engine/compound-share.d.ts +85 -0
- package/dist/engine/compound-share.js +606 -0
- package/dist/engine/correction-cluster-runner.d.ts +38 -0
- package/dist/engine/correction-cluster-runner.js +188 -0
- package/dist/engine/correction-clustering.d.ts +80 -0
- package/dist/engine/correction-clustering.js +167 -0
- package/dist/engine/enforce-classifier.d.ts +10 -1
- package/dist/engine/enforce-classifier.js +111 -22
- package/dist/engine/extraction-session.js +10 -3
- package/dist/engine/private-filter.d.ts +36 -0
- package/dist/engine/private-filter.js +100 -0
- package/dist/engine/ranking-pipeline.js +4 -2
- package/dist/engine/relevance-gate.d.ts +12 -0
- package/dist/engine/relevance-gate.js +12 -0
- package/dist/engine/roi-demotion.d.ts +79 -0
- package/dist/engine/roi-demotion.js +159 -0
- package/dist/engine/solution-format.d.ts +1 -0
- package/dist/engine/solution-format.js +26 -0
- package/dist/engine/solution-matcher.js +10 -1
- package/dist/fgx.js +7 -6
- package/dist/forge/cli.js +8 -2
- package/dist/hooks/compound-reflection.js +6 -1
- package/dist/hooks/context-guard.d.ts +16 -1
- package/dist/hooks/context-guard.js +99 -49
- package/dist/hooks/hook-config.js +5 -0
- package/dist/hooks/hook-registry.d.ts +6 -1
- package/dist/hooks/hooks-generator.js +3 -0
- package/dist/hooks/post-tool-use.js +2 -3
- package/dist/hooks/pre-compact.js +14 -0
- package/dist/hooks/pre-tool-use.js +5 -1
- package/dist/hooks/session-end.d.ts +34 -0
- package/dist/hooks/session-end.js +100 -0
- package/dist/hooks/session-recovery.js +18 -1
- package/dist/hooks/shared/hook-timing.js +6 -1
- package/dist/hooks/shared/stop-triggers.d.ts +29 -2
- package/dist/hooks/shared/stop-triggers.js +35 -2
- package/dist/hooks/solution-injector.d.ts +28 -0
- package/dist/hooks/solution-injector.js +126 -28
- package/dist/hooks/stop-guard.js +5 -2
- package/dist/hooks/subagent-stop-guard.js +4 -1
- package/dist/host/capabilities-claude.js +1 -0
- package/dist/host/capabilities-codex.js +1 -0
- package/dist/host/capabilities-opencode.d.ts +26 -0
- package/dist/host/capabilities-opencode.js +78 -0
- package/dist/host/capabilities-registry.d.ts +7 -0
- package/dist/host/capabilities-registry.js +14 -0
- package/dist/host/codex-rules-context.d.ts +25 -0
- package/dist/host/codex-rules-context.js +62 -0
- package/dist/host/exec-host.d.ts +14 -3
- package/dist/host/exec-host.js +15 -2
- package/dist/host/host-binding.d.ts +27 -0
- package/dist/host/host-binding.js +11 -0
- package/dist/host/host-runtime.js +18 -0
- package/dist/host/install-codex.d.ts +66 -0
- package/dist/host/install-codex.js +293 -25
- package/dist/host/install-opencode.d.ts +38 -0
- package/dist/host/install-opencode.js +148 -0
- package/dist/host/install-orchestrator.d.ts +4 -1
- package/dist/host/install-orchestrator.js +30 -1
- package/dist/host/invoke-agent.d.ts +3 -2
- package/dist/host/invoke-agent.js +1 -0
- package/dist/host/opencode/context-cli.d.ts +15 -0
- package/dist/host/opencode/context-cli.js +25 -0
- package/dist/host/opencode/guard-cli.d.ts +13 -0
- package/dist/host/opencode/guard-cli.js +39 -0
- package/dist/host/opencode/plugin/forgen.d.ts +31 -0
- package/dist/host/opencode/plugin/forgen.js +60 -0
- package/dist/host/opencode/translate.d.ts +49 -0
- package/dist/host/opencode/translate.js +96 -0
- package/dist/host/parity-harness.d.ts +10 -2
- package/dist/host/parity-harness.js +7 -3
- package/dist/host/projection.d.ts +37 -3
- package/dist/host/projection.js +104 -42
- package/dist/mcp/tools.js +17 -4
- package/dist/store/evidence-store.d.ts +2 -6
- package/dist/store/evidence-store.js +164 -46
- package/dist/store/host-mismatch.d.ts +2 -1
- package/dist/store/host-mismatch.js +8 -8
- package/dist/store/profile-store.d.ts +3 -2
- package/dist/store/rule-store.d.ts +41 -0
- package/dist/store/rule-store.js +77 -2
- package/dist/store/types.d.ts +9 -2
- package/hooks/hooks.json +13 -1
- package/package.json +3 -2
- package/plugin.json +2 -2
- package/skills/architecture-decision/SKILL.md +18 -0
- package/skills/calibrate/SKILL.md +18 -0
- package/skills/code-review/SKILL.md +17 -0
- package/skills/compound/SKILL.md +17 -0
- package/skills/deep-interview/SKILL.md +7 -0
- package/skills/docker/SKILL.md +18 -0
- package/skills/forge-loop/SKILL.md +30 -1
- package/skills/learn/SKILL.md +15 -0
- package/skills/retro/SKILL.md +16 -0
- package/skills/ship/SKILL.md +36 -1
package/dist/core/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as os from 'node:os';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES,
|
|
6
|
+
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, PACKS_DIR, SESSIONS_DIR, STATE_DIR, V1_SESSIONS_DIR } from './paths.js';
|
|
7
7
|
import { getTimingStats } from '../hooks/shared/hook-timing.js';
|
|
8
8
|
import { countSessionScopedFiles, pruneState } from './state-gc.js';
|
|
9
9
|
import { summarizeAllByHost } from '../store/host-mismatch.js';
|
|
@@ -52,12 +52,39 @@ function relativeTime(isoString) {
|
|
|
52
52
|
}
|
|
53
53
|
return `${diffDays}d ago`;
|
|
54
54
|
}
|
|
55
|
+
/** ADR-014 D4 — [Codex Hooks] 섹션: hooks.json forgen 엔트리의 Codex trust 기록 대조 */
|
|
56
|
+
async function renderCodexHookTrust() {
|
|
57
|
+
try {
|
|
58
|
+
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
|
|
59
|
+
const hooksPath = path.join(codexHome, 'hooks.json');
|
|
60
|
+
if (!fs.existsSync(hooksPath))
|
|
61
|
+
return; // Codex 미설치/미등록 — 섹션 생략
|
|
62
|
+
const { auditCodexHookTrust } = await import('../host/install-codex.js');
|
|
63
|
+
// pkgRoot: dist/core/doctor.js → 두 단계 위. 어차피 FORGEN_HOOK_SCRIPT_MARKER fallback 이 있어 정확도 비의존.
|
|
64
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
65
|
+
const t = auditCodexHookTrust({ hooksPath, configTomlPath: path.join(codexHome, 'config.toml'), pkgRoot });
|
|
66
|
+
console.log(' [Codex Hooks]');
|
|
67
|
+
if (t.total === 0) {
|
|
68
|
+
console.log(' △ hooks.json 에 forgen hook 없음 — forgen install codex');
|
|
69
|
+
}
|
|
70
|
+
else if (t.trusted === t.total) {
|
|
71
|
+
console.log(` ✓ ${t.trusted}/${t.total} forgen hooks trusted by Codex`);
|
|
72
|
+
}
|
|
73
|
+
else if (t.noStateRecorded) {
|
|
74
|
+
console.log(` △ ${t.total} forgen hooks registered, Codex trust 기록 없음 — codex 안에서 /hooks 로 승인 (미승인 훅은 skip 됨)`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(` ✗ ${t.untrusted.length}/${t.total} forgen hooks untrusted (${t.untrusted.slice(0, 4).join(', ')}${t.untrusted.length > 4 ? ', …' : ''}) — codex 안에서 /hooks 로 승인`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* fail-open */ }
|
|
81
|
+
}
|
|
55
82
|
/** [Codex Parity] 섹션 렌더링 — ~/.forgen/state/parity-result.json 신선도 검사 */
|
|
56
83
|
function renderCodexParity() {
|
|
57
84
|
console.log(' [Codex Parity]');
|
|
58
85
|
const parityPath = path.join(STATE_DIR, 'parity-result.json');
|
|
59
86
|
if (!fs.existsSync(parityPath)) {
|
|
60
|
-
console.log(' △ Codex parity 미실행 — tests/e2e/codex/run-parity.sh 또는 forgen parity codex');
|
|
87
|
+
console.log(' △ Codex parity 미실행 — tests/e2e/codex/run-parity.sh 또는 forgen dev parity codex');
|
|
61
88
|
return;
|
|
62
89
|
}
|
|
63
90
|
let data;
|
|
@@ -90,10 +117,51 @@ function renderCodexParity() {
|
|
|
90
117
|
console.log(` ✓ Codex parity green (last run: ${timeStr},${version})`);
|
|
91
118
|
}
|
|
92
119
|
}
|
|
120
|
+
/** plugin cache 디렉토리에 버전 엔트리가 하나 이상 있는가 (check + repair 재검증 공용) */
|
|
121
|
+
export function pluginCacheOk() {
|
|
122
|
+
const pluginCacheBase = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'forgen-local', 'forgen');
|
|
123
|
+
if (!exists(pluginCacheBase))
|
|
124
|
+
return false;
|
|
125
|
+
try {
|
|
126
|
+
return fs.readdirSync(pluginCacheBase).some(f => {
|
|
127
|
+
try {
|
|
128
|
+
const lstat = fs.lstatSync(path.join(pluginCacheBase, f));
|
|
129
|
+
return lstat.isDirectory() || lstat.isSymbolicLink();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** installed_plugins.json 의 forgen entry 가 존재하며 installPath 가 살아있는가 */
|
|
141
|
+
export function pluginRegisteredOk() {
|
|
142
|
+
const installedPluginsPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
143
|
+
if (!exists(installedPluginsPath))
|
|
144
|
+
return false;
|
|
145
|
+
try {
|
|
146
|
+
const installed = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
|
|
147
|
+
const entry = installed?.plugins?.['forgen@forgen-local'];
|
|
148
|
+
if (Array.isArray(entry) && entry.length > 0) {
|
|
149
|
+
const installPath = entry[0]?.installPath;
|
|
150
|
+
return !!installPath && exists(installPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* ignore */ }
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
93
156
|
/**
|
|
94
157
|
* v0.4.8 (E3): plugin cache / installPath 진단이 실패했을 때 자동 복구.
|
|
95
|
-
* forgen 패키지 디렉토리에서 `npm run build` + postinstall 을 차례로 실행.
|
|
96
158
|
* 실패해도 doctor 자체는 계속 진행 (fail-open).
|
|
159
|
+
*
|
|
160
|
+
* W1-4 수정 (ADR-010, 실측 2026-07-16): 이전 구현은 무조건 `npm run build` 를
|
|
161
|
+
* 먼저 실행했는데, 글로벌 설치엔 devDeps(tsc)가 없어 MODULE_NOT_FOUND 로 실패
|
|
162
|
+
* → postinstall 에 도달하지 못해 캐시가 영원히 복구되지 않았다. published
|
|
163
|
+
* 패키지는 dist 가 이미 있으므로 build 는 dist 부재(dev checkout)시에만 시도.
|
|
164
|
+
* 또한 "실행했다"가 아니라 "복구됐다"를 재검증 후 보고한다.
|
|
97
165
|
*/
|
|
98
166
|
function attemptPluginRepair() {
|
|
99
167
|
try {
|
|
@@ -102,20 +170,53 @@ function attemptPluginRepair() {
|
|
|
102
170
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
103
171
|
const pkgRoot = path.resolve(here, '..', '..');
|
|
104
172
|
console.log(`\n [Repair] forgen 패키지 자가복구 시도 — ${pkgRoot}`);
|
|
105
|
-
|
|
173
|
+
if (!exists(path.join(pkgRoot, 'dist', 'cli.js'))) {
|
|
174
|
+
// dev checkout 등 dist 부재 시에만 build (devDeps 가 있는 환경)
|
|
175
|
+
execFileSync('npm', ['run', 'build'], { cwd: pkgRoot, stdio: 'inherit' });
|
|
176
|
+
}
|
|
106
177
|
execFileSync('node', ['scripts/postinstall.js'], { cwd: pkgRoot, stdio: 'inherit' });
|
|
107
|
-
|
|
108
|
-
|
|
178
|
+
// 결과 재검증 — 성공 주장은 검증된 상태에만 한다.
|
|
179
|
+
const ok = pluginCacheOk() && pluginRegisteredOk();
|
|
180
|
+
if (ok) {
|
|
181
|
+
console.log(' [Repair] ✓ 복구 확인 — plugin cache/registry 재검증 통과');
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.warn(' [Repair] ✗ postinstall 은 실행됐지만 재검증 실패 — 수동 확인 필요');
|
|
185
|
+
console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> && node scripts/postinstall.js');
|
|
186
|
+
}
|
|
187
|
+
return ok;
|
|
109
188
|
}
|
|
110
189
|
catch (e) {
|
|
111
190
|
console.warn(` [Repair] 실패: ${e instanceof Error ? e.message : String(e)}`);
|
|
112
|
-
console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> &&
|
|
191
|
+
console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> && node scripts/postinstall.js (dist 부재 시 npm run build 선행)');
|
|
113
192
|
return false;
|
|
114
193
|
}
|
|
115
194
|
}
|
|
195
|
+
/** W1-1: --reclaim 스캔 (읽기 전용) — full/quick 양쪽에서 재사용 */
|
|
196
|
+
async function runReclaimScan() {
|
|
197
|
+
try {
|
|
198
|
+
const { runReclaim, printReclaimResult } = await import('./migrate-tenetx.js');
|
|
199
|
+
printReclaimResult(runReclaim({ cwd: process.cwd(), dryRun: true }));
|
|
200
|
+
console.log(' 실제 회수: forgen migrate tenetx [--yes] [--apply-settings]');
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
console.warn(` [reclaim] 스캔 실패: ${e instanceof Error ? e.message : String(e)}`);
|
|
204
|
+
}
|
|
205
|
+
console.log();
|
|
206
|
+
}
|
|
207
|
+
/** W1-4: repair 성공 시 Summary 에서 걷어내는 대상 — check() 라벨과 단일 소스 */
|
|
208
|
+
const REPAIRABLE_PLUGIN_LABELS = Object.freeze({
|
|
209
|
+
cache: 'forgen plugin cache',
|
|
210
|
+
registered: 'forgen plugin registered & installPath exists',
|
|
211
|
+
});
|
|
116
212
|
export async function runDoctor(opts = {}) {
|
|
117
213
|
failedChecks = [];
|
|
118
|
-
console.log('\n Forgen — Diagnostics
|
|
214
|
+
console.log('\n Forgen — Diagnostics');
|
|
215
|
+
// ADR-010 W2-1 경계 선언: 일반 환경 건강(컨텍스트 비용, unused skill/MCP,
|
|
216
|
+
// CLAUDE.md 정리, slow hook)은 native /doctor 의 영역. 이 명령은 forgen
|
|
217
|
+
// 자체 기계(플러그인 캐시·훅 오류·상태 위생)와 효과-측정 게이트만 본다.
|
|
218
|
+
console.log(' 환경 전반 진단은 Claude Code native /doctor 를 사용하세요.');
|
|
219
|
+
console.log(' 이 명령은 forgen 자체 기계와 효과-측정 게이트를 검사합니다.\n');
|
|
119
220
|
section('Tools');
|
|
120
221
|
check('claude CLI', commandExists('claude'));
|
|
121
222
|
check('tmux', commandExists('tmux'));
|
|
@@ -125,46 +226,24 @@ export async function runDoctor(opts = {}) {
|
|
|
125
226
|
section('Plugins');
|
|
126
227
|
const ralphLoopInstalled = exists(path.join(os.homedir(), '.claude', 'plugins', 'cache', 'claude-plugins-official', 'ralph-loop'));
|
|
127
228
|
check('ralph-loop plugin', ralphLoopInstalled, 'Required for ralph mode auto-iteration. Install: claude plugins install ralph-loop');
|
|
128
|
-
// forgen 플러그인 캐시
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (exists(pluginCacheBase)) {
|
|
132
|
-
const versions = fs.readdirSync(pluginCacheBase).filter(f => {
|
|
133
|
-
try {
|
|
134
|
-
const lstat = fs.lstatSync(path.join(pluginCacheBase, f));
|
|
135
|
-
return lstat.isDirectory() || lstat.isSymbolicLink();
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
forgenPluginCacheOk = versions.length > 0;
|
|
142
|
-
}
|
|
143
|
-
check('forgen plugin cache', forgenPluginCacheOk, opts.repair
|
|
229
|
+
// forgen 플러그인 캐시 / registry 정합성 — 훅 실행의 필수 전제
|
|
230
|
+
const forgenPluginCacheOk = pluginCacheOk();
|
|
231
|
+
check(REPAIRABLE_PLUGIN_LABELS.cache, forgenPluginCacheOk, opts.repair
|
|
144
232
|
? 'Hook execution requires plugin cache. Attempting auto-repair (--repair)…'
|
|
145
|
-
: 'Hook execution requires plugin cache. Fix:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let pluginRegistered = false;
|
|
149
|
-
if (exists(installedPluginsPath)) {
|
|
150
|
-
try {
|
|
151
|
-
const installed = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
|
|
152
|
-
const entry = installed?.plugins?.['forgen@forgen-local'];
|
|
153
|
-
if (Array.isArray(entry) && entry.length > 0) {
|
|
154
|
-
const installPath = entry[0]?.installPath;
|
|
155
|
-
pluginRegistered = !!installPath && exists(installPath);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
catch { /* ignore */ }
|
|
159
|
-
}
|
|
160
|
-
check('forgen plugin registered & installPath exists', pluginRegistered, opts.repair
|
|
233
|
+
: 'Hook execution requires plugin cache. Fix: node scripts/postinstall.js in the forgen package (or rerun with --repair)');
|
|
234
|
+
const pluginRegistered = pluginRegisteredOk();
|
|
235
|
+
check(REPAIRABLE_PLUGIN_LABELS.registered, pluginRegistered, opts.repair
|
|
161
236
|
? 'Plugin registered but installPath missing on disk. Attempting auto-repair (--repair)…'
|
|
162
|
-
: 'Plugin registered but installPath missing on disk. Fix:
|
|
163
|
-
// v0.4.8 (E3): plugin cache 또는 installPath 가 깨졌고 --repair 가
|
|
164
|
-
// 있으면
|
|
165
|
-
//
|
|
237
|
+
: 'Plugin registered but installPath missing on disk. Fix: node scripts/postinstall.js in the forgen package (or rerun with --repair)');
|
|
238
|
+
// v0.4.8 (E3) + W1-4: plugin cache 또는 installPath 가 깨졌고 --repair 가
|
|
239
|
+
// 켜져 있으면 자가복구. 재검증까지 통과하면 failedChecks 에서 해당 항목을
|
|
240
|
+
// 걷어내 Summary 가 복구된 상태를 정직하게 반영하게 한다.
|
|
166
241
|
if (opts.repair && (!forgenPluginCacheOk || !pluginRegistered)) {
|
|
167
|
-
attemptPluginRepair();
|
|
242
|
+
const repaired = attemptPluginRepair();
|
|
243
|
+
if (repaired) {
|
|
244
|
+
const pluginLabels = new Set(Object.values(REPAIRABLE_PLUGIN_LABELS));
|
|
245
|
+
failedChecks = failedChecks.filter(f => !pluginLabels.has(f.label));
|
|
246
|
+
}
|
|
168
247
|
}
|
|
169
248
|
console.log();
|
|
170
249
|
section('Directories');
|
|
@@ -204,6 +283,9 @@ export async function runDoctor(opts = {}) {
|
|
|
204
283
|
}
|
|
205
284
|
console.log();
|
|
206
285
|
if (opts.quick) {
|
|
286
|
+
// --reclaim 은 읽기 전용 스캔이라 --quick 과 조합 가능 (silent-ignore 방지)
|
|
287
|
+
if (opts.reclaim)
|
|
288
|
+
await runReclaimScan();
|
|
207
289
|
console.log();
|
|
208
290
|
if (failedChecks.length === 0) {
|
|
209
291
|
console.log(' All essential checks passed.\n');
|
|
@@ -213,9 +295,32 @@ export async function runDoctor(opts = {}) {
|
|
|
213
295
|
}
|
|
214
296
|
return;
|
|
215
297
|
}
|
|
298
|
+
// 리뷰 SEV-3 (c): critic-review 룰의 트리거가 stale(skip-review 시그널 미포함)이면 발견 nudge.
|
|
299
|
+
// "고쳤는데 기존 유저에겐 안 닿는" 상태 방지 — 재-bake 명령을 안내한다.
|
|
300
|
+
section('Rules');
|
|
301
|
+
try {
|
|
302
|
+
const [{ loadActiveRules }, { needsCriticTriggerMigration }] = await Promise.all([
|
|
303
|
+
import('../store/rule-store.js'),
|
|
304
|
+
import('../engine/enforce-classifier.js'),
|
|
305
|
+
]);
|
|
306
|
+
const outdated = loadActiveRules().filter(needsCriticTriggerMigration);
|
|
307
|
+
check('critic-review 룰 트리거 최신', outdated.length === 0, `${outdated.length}개 critic 룰이 구 트리거(skip-review 미포함) — \`forgen rule classify --apply --force\` 로 갱신 권장`);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
/* rule-store 로드 실패는 무해 — 스킵 */
|
|
311
|
+
}
|
|
312
|
+
console.log();
|
|
216
313
|
section('Environment');
|
|
217
314
|
check('Inside tmux session', !!process.env.TMUX, 'FORGEN auto-compound relies on tmux. Launch: tmux new -s forgen');
|
|
218
315
|
check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1', 'Set by `forgen` / `fgx` launcher. Hooks assume harness mode is active.');
|
|
316
|
+
// ADR-012: auto-compound Haiku 추출 동의 상태 고지 (transcript 요약 API 전송 여부).
|
|
317
|
+
try {
|
|
318
|
+
const { getHaikuConsentState } = await import('./compound-consent.js');
|
|
319
|
+
const s = getHaikuConsentState();
|
|
320
|
+
console.log(` auto-compound Haiku 추출: ${s.enabled ? 'ON' : 'OFF'} (${s.source}) — ` +
|
|
321
|
+
`${s.enabled ? '세션 요약이 Haiku 로 전송됨' : '결정론 룰 승급만(egress 0)'}. 변경: forgen compound consent on|off`);
|
|
322
|
+
}
|
|
323
|
+
catch { /* noop */ }
|
|
219
324
|
console.log();
|
|
220
325
|
// v0.4.1 파일 확장자 버그 수정: rules 는 .json, behavior 도 대부분 .json 포맷.
|
|
221
326
|
// 이전에 .md 만 count 해서 실 rules 4개인데 0 으로 표시되는 incident 관찰.
|
|
@@ -301,26 +406,28 @@ export async function runDoctor(opts = {}) {
|
|
|
301
406
|
console.log(' Unable to read hook error log.');
|
|
302
407
|
}
|
|
303
408
|
console.log();
|
|
304
|
-
// Hook Timing:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.log(' Hook Count p50ms p95ms max ms');
|
|
312
|
-
console.log(` ${'-'.repeat(56)}`);
|
|
313
|
-
for (const s of timingStats) {
|
|
314
|
-
const hook = s.hook.padEnd(22);
|
|
315
|
-
const count = String(s.count).padStart(5);
|
|
316
|
-
const p50 = String(s.p50).padStart(7);
|
|
317
|
-
const p95 = String(s.p95).padStart(7);
|
|
318
|
-
const max = String(s.max).padStart(8);
|
|
319
|
-
console.log(` ${hook}${count}${p50}${p95}${max}`);
|
|
409
|
+
// Hook Timing: W2-1 (ADR-010) — slow hook 감지는 native /doctor 가 하므로
|
|
410
|
+
// 표시는 --verbose 로 강등. 수집(hook-timing.jsonl)은 자체 회귀 테스트용으로 유지.
|
|
411
|
+
if (opts.verbose) {
|
|
412
|
+
console.log(' [Hook Timing]');
|
|
413
|
+
const timingStats = getTimingStats();
|
|
414
|
+
if (timingStats.length === 0) {
|
|
415
|
+
console.log(' No timing data collected yet.');
|
|
320
416
|
}
|
|
417
|
+
else {
|
|
418
|
+
console.log(' Hook Count p50ms p95ms max ms');
|
|
419
|
+
console.log(` ${'-'.repeat(56)}`);
|
|
420
|
+
for (const s of timingStats) {
|
|
421
|
+
const hook = s.hook.padEnd(22);
|
|
422
|
+
const count = String(s.count).padStart(5);
|
|
423
|
+
const p50 = String(s.p50).padStart(7);
|
|
424
|
+
const p95 = String(s.p95).padStart(7);
|
|
425
|
+
const max = String(s.max).padStart(8);
|
|
426
|
+
console.log(` ${hook}${count}${p50}${p95}${max}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
console.log();
|
|
321
430
|
}
|
|
322
|
-
console.log();
|
|
323
|
-
console.log();
|
|
324
431
|
// v1: 팀 팩 시스템 제거. 개인 모드만 지원.
|
|
325
432
|
console.log(' [Pack Connections]');
|
|
326
433
|
console.log(' v1: Personal mode only (team packs removed)');
|
|
@@ -365,107 +472,9 @@ export async function runDoctor(opts = {}) {
|
|
|
365
472
|
console.log();
|
|
366
473
|
}
|
|
367
474
|
}
|
|
368
|
-
// Harness Maturity
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// 1. Preparation
|
|
372
|
-
const hasClaude = fs.existsSync(path.join(cwd, 'CLAUDE.md'));
|
|
373
|
-
let rulesCount = 0;
|
|
374
|
-
try {
|
|
375
|
-
const rulesDir = path.join(cwd, '.claude', 'rules');
|
|
376
|
-
if (fs.existsSync(rulesDir)) {
|
|
377
|
-
rulesCount = fs.readdirSync(rulesDir).filter(f => f.endsWith('.md')).length;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
catch { /* fail-open */ }
|
|
381
|
-
let hooksActive = 0;
|
|
382
|
-
try {
|
|
383
|
-
const hooksJsonPath = path.join(cwd, 'hooks', 'hooks.json');
|
|
384
|
-
if (fs.existsSync(hooksJsonPath)) {
|
|
385
|
-
const hooksData = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
|
|
386
|
-
if (hooksData.hooks && typeof hooksData.hooks === 'object') {
|
|
387
|
-
for (const eventHooks of Object.values(hooksData.hooks)) {
|
|
388
|
-
if (Array.isArray(eventHooks)) {
|
|
389
|
-
for (const group of eventHooks) {
|
|
390
|
-
if (Array.isArray(group.hooks)) {
|
|
391
|
-
hooksActive += (group.hooks).length;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
catch { /* fail-open */ }
|
|
400
|
-
const prepL = hasClaude && rulesCount >= 3 && hooksActive > 0 ? 'L3' : hasClaude && hooksActive > 0 ? 'L2' : hasClaude ? 'L1' : 'L0';
|
|
401
|
-
// 2. Context
|
|
402
|
-
let solutionsCount = 0;
|
|
403
|
-
try {
|
|
404
|
-
if (exists(ME_SOLUTIONS))
|
|
405
|
-
solutionsCount = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md')).length;
|
|
406
|
-
}
|
|
407
|
-
catch { /* fail-open */ }
|
|
408
|
-
let behaviorCount = 0;
|
|
409
|
-
try {
|
|
410
|
-
if (exists(ME_BEHAVIOR))
|
|
411
|
-
behaviorCount = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.md')).length;
|
|
412
|
-
}
|
|
413
|
-
catch { /* fail-open */ }
|
|
414
|
-
const ctxL = solutionsCount >= 5 && behaviorCount >= 3 ? 'L3' : solutionsCount >= 3 || behaviorCount >= 1 ? 'L2' : solutionsCount > 0 || behaviorCount > 0 ? 'L1' : 'L0';
|
|
415
|
-
// 3. Execution
|
|
416
|
-
const hasSkills = exists(ME_SKILLS);
|
|
417
|
-
const execL = hasSkills ? 'L2' : 'L1';
|
|
418
|
-
// 4. Validation
|
|
419
|
-
const hasTests = fs.existsSync(path.join(cwd, 'tests'));
|
|
420
|
-
const hasCI = fs.existsSync(path.join(cwd, '.github', 'workflows'));
|
|
421
|
-
const validL = hasTests && hasCI ? 'L3' : hasTests ? 'L2' : 'L1';
|
|
422
|
-
// 5. Improvement: reflection rate from solutions
|
|
423
|
-
let reflectionRate = 0;
|
|
424
|
-
try {
|
|
425
|
-
if (exists(ME_SOLUTIONS)) {
|
|
426
|
-
const solFiles = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
|
|
427
|
-
if (solFiles.length > 0) {
|
|
428
|
-
let reflected = 0;
|
|
429
|
-
for (const f of solFiles) {
|
|
430
|
-
try {
|
|
431
|
-
const content = fs.readFileSync(path.join(ME_SOLUTIONS, f), 'utf-8');
|
|
432
|
-
const match = content.match(/reflected:\s*(\d+)/);
|
|
433
|
-
if (match && parseInt(match[1], 10) > 0)
|
|
434
|
-
reflected++;
|
|
435
|
-
}
|
|
436
|
-
catch { /* skip */ }
|
|
437
|
-
}
|
|
438
|
-
reflectionRate = Math.round((reflected / solFiles.length) * 100);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
catch { /* fail-open */ }
|
|
443
|
-
const improvL = reflectionRate > 0 ? 'L3' : solutionsCount > 0 ? 'L2' : 'L1';
|
|
444
|
-
const levelIcon = (l) => l === 'L3' ? '✓' : l === 'L2' ? '✓' : l === 'L1' ? '✗' : '✗';
|
|
445
|
-
console.log(` Axis Level Detail`);
|
|
446
|
-
console.log(` ${'─'.repeat(55)}`);
|
|
447
|
-
console.log(` ${levelIcon(prepL)} Preparation ${prepL} CLAUDE.md:${hasClaude ? 'yes' : 'no'}, rules:${rulesCount}, hooks:${hooksActive}`);
|
|
448
|
-
console.log(` ${levelIcon(ctxL)} Context ${ctxL} solutions:${solutionsCount}, behavior:${behaviorCount}`);
|
|
449
|
-
console.log(` ${levelIcon(execL)} Execution ${execL} skills:${hasSkills ? 'yes' : 'no'}`);
|
|
450
|
-
console.log(` ${levelIcon(validL)} Validation ${validL} tests:${hasTests ? 'yes' : 'no'}, CI:${hasCI ? 'yes' : 'no'}`);
|
|
451
|
-
console.log(` ${levelIcon(improvL)} Improvement ${improvL} reflection:${reflectionRate}%`);
|
|
452
|
-
console.log();
|
|
453
|
-
// Quick wins: suggest for lowest scoring axes
|
|
454
|
-
const axes = [
|
|
455
|
-
{ name: 'Preparation', level: prepL, hint: 'Add CLAUDE.md + .claude/rules/ files' },
|
|
456
|
-
{ name: 'Context', level: ctxL, hint: 'Run /compound to accumulate solutions' },
|
|
457
|
-
{ name: 'Execution', level: execL, hint: 'Promote solutions to skills' },
|
|
458
|
-
{ name: 'Validation', level: validL, hint: 'Add tests/ dir and .github/workflows' },
|
|
459
|
-
{ name: 'Improvement', level: improvL, hint: 'Reflect on existing solutions' },
|
|
460
|
-
];
|
|
461
|
-
const quickWins = axes.filter(a => a.level === 'L0' || a.level === 'L1').slice(0, 3);
|
|
462
|
-
if (quickWins.length > 0) {
|
|
463
|
-
console.log(' Quick Wins (Top 3):');
|
|
464
|
-
for (const win of quickWins) {
|
|
465
|
-
console.log(` → ${win.name}: ${win.hint}`);
|
|
466
|
-
}
|
|
467
|
-
console.log();
|
|
468
|
-
}
|
|
475
|
+
// W2-1 (ADR-010): Harness Maturity / Quick Wins 섹션 제거 —
|
|
476
|
+
// "CLAUDE.md 추가하세요" 류 일반 셋업 조언은 native /doctor 영역.
|
|
477
|
+
// forgen 고유 진단(아래 State Hygiene, ψ-long, parity 등)만 유지한다.
|
|
469
478
|
// State bloat check — session-scoped files accumulate until pruned.
|
|
470
479
|
console.log(' [State Hygiene]');
|
|
471
480
|
const sessionFiles = countSessionScopedFiles();
|
|
@@ -525,7 +534,7 @@ export async function runDoctor(opts = {}) {
|
|
|
525
534
|
if (map.available && map.hotspots.length > 0) {
|
|
526
535
|
const top = map.hotspots.map((h) => `${h.path} (${h.fixHits})`).join(', ');
|
|
527
536
|
console.log(` → 진앙 후보 top 3: ${top}`);
|
|
528
|
-
console.log(' → 전체 보기: forgen regress-map');
|
|
537
|
+
console.log(' → 전체 보기: forgen dev regress-map');
|
|
529
538
|
}
|
|
530
539
|
}
|
|
531
540
|
}
|
|
@@ -556,6 +565,8 @@ export async function runDoctor(opts = {}) {
|
|
|
556
565
|
console.log(' Unable to read host evidence data.');
|
|
557
566
|
}
|
|
558
567
|
console.log();
|
|
568
|
+
// [Codex Hooks] — ADR-014 D4 trust 감사
|
|
569
|
+
await renderCodexHookTrust();
|
|
559
570
|
// [Codex Parity] — parity-result.json 신선도 검사 (v0.4.2 패턴 확장)
|
|
560
571
|
renderCodexParity();
|
|
561
572
|
console.log();
|
|
@@ -589,20 +600,23 @@ export async function runDoctor(opts = {}) {
|
|
|
589
600
|
console.log(` Unable to read psi-long state: ${e instanceof Error ? e.message : 'unknown'}`);
|
|
590
601
|
}
|
|
591
602
|
console.log();
|
|
592
|
-
// [Effort
|
|
593
|
-
//
|
|
594
|
-
|
|
603
|
+
// [Effort] — ADR-009 §5 nudge-only. W2-1 (ADR-010): Sonnet 5 는 adaptive
|
|
604
|
+
// thinking 으로 effort 를 자체 관리하므로, 권고가 실제로 의미 있는 상황
|
|
605
|
+
// (forge-loop 활성 = long-running → xhigh 넛지)에서만 표시한다.
|
|
595
606
|
try {
|
|
596
607
|
const loopActive = !!readForgeLoopState()?.active;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
608
|
+
if (loopActive) {
|
|
609
|
+
const adv = effortAdvisory({ longRunningActive: true });
|
|
610
|
+
console.log(' [Effort]');
|
|
611
|
+
console.log(` → recommend: ${adv.recommend}`);
|
|
612
|
+
console.log(` ${adv.reason}`);
|
|
613
|
+
console.log();
|
|
614
|
+
}
|
|
604
615
|
}
|
|
605
|
-
|
|
616
|
+
catch { /* effort 권고 실패는 침묵 — 부가 정보일 뿐 */ }
|
|
617
|
+
// W1-1 (ADR-010): --reclaim → legacy 규칙 스프롤 스캔 (읽기 전용).
|
|
618
|
+
if (opts.reclaim)
|
|
619
|
+
await runReclaimScan();
|
|
606
620
|
// [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
|
|
607
621
|
console.log(' [Summary]');
|
|
608
622
|
if (failedChecks.length === 0) {
|
|
@@ -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';
|
package/dist/core/drift-score.js
CHANGED
|
@@ -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,
|