@wooojin/forgen 0.4.0 → 0.4.3
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 +5 -5
- package/CHANGELOG.md +194 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +74 -9
- package/README.ko.md +77 -12
- package/README.md +127 -25
- package/README.zh.md +43 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
- package/dist/checks/conclusion-verification-ratio.js +86 -0
- package/dist/checks/fact-vs-agreement.d.ts +47 -0
- package/dist/checks/fact-vs-agreement.js +92 -0
- package/dist/checks/self-score-deflation.d.ts +38 -0
- package/dist/checks/self-score-deflation.js +108 -0
- package/dist/cli.js +98 -6
- package/dist/core/auto-compound-runner.js +137 -49
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +41 -2
- package/dist/core/doctor.js +118 -5
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -0
- package/dist/core/init-cli.d.ts +26 -0
- package/dist/core/init-cli.js +104 -0
- package/dist/core/init.js +17 -0
- package/dist/core/inspect-cli.js +1 -2
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +11 -0
- package/dist/core/migrate-cli.js +53 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/paths.d.ts +8 -1
- package/dist/core/paths.js +11 -2
- package/dist/core/recall-cli.d.ts +26 -0
- package/dist/core/recall-cli.js +125 -0
- package/dist/core/recall-reference-detector.d.ts +43 -0
- package/dist/core/recall-reference-detector.js +65 -0
- package/dist/core/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.d.ts +21 -0
- package/dist/core/stats-cli.js +133 -10
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/core/uninstall.js +2 -1
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +5 -6
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
- package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
- package/dist/engine/lifecycle/orchestrator.js +2 -2
- package/dist/engine/lifecycle/signals.js +6 -6
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
- package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
- package/dist/engine/skill-promoter.js +3 -6
- package/dist/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/context-guard.js +1 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +21 -5
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +2 -2
- package/dist/hooks/notepad-injector.js +1 -1
- package/dist/hooks/permission-handler.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +7 -1
- package/dist/hooks/post-tool-use.js +50 -23
- package/dist/hooks/pre-compact.js +2 -2
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +28 -10
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +1 -1
- package/dist/hooks/session-recovery.js +12 -1
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +30 -2
- package/dist/hooks/shared/hook-response.js +61 -3
- package/dist/hooks/skill-injector.js +2 -2
- package/dist/hooks/slop-detector.js +2 -2
- package/dist/hooks/solution-injector.d.ts +9 -0
- package/dist/hooks/solution-injector.js +48 -5
- package/dist/hooks/stop-guard.js +152 -13
- package/dist/hooks/subagent-tracker.js +1 -1
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/i18n/index.js +3 -5
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +45 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/rule-store.js +8 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +7 -5
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/calibrate.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/retro.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen init` CLI
|
|
3
|
+
*
|
|
4
|
+
* 빈 FORGEN_HOME (또는 기존에 starter 미설치 홈) 에 starter-pack 솔루션을
|
|
5
|
+
* 프로비저닝. npm install-g 시의 postinstall 이 하던 starter 배포 로직을 런타임
|
|
6
|
+
* CLI 로 노출해 다음 시나리오 지원:
|
|
7
|
+
* - `FORGEN_HOME=/tmp/fresh forgen init` — 격리 테스트 환경
|
|
8
|
+
* - CI pipeline 신규 컨테이너 프로비저닝
|
|
9
|
+
* - 사용자가 실수로 me/solutions 전부 삭제한 뒤 복구
|
|
10
|
+
*
|
|
11
|
+
* 보수적 정책: me/solutions 에 **≥5개 파일**이 이미 있으면 건너뜀 (사용자
|
|
12
|
+
* 실 축적물 보호). `--force` 플래그로 우회 가능. postinstall 의 installStarterPack
|
|
13
|
+
* 과 동일 규칙.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { ME_DIR } from './paths.js';
|
|
19
|
+
/** 패키지 루트의 starter-pack/solutions 디렉터리. */
|
|
20
|
+
function findStarterDir() {
|
|
21
|
+
// 런타임에 dist/core/init-cli.js — 패키지 루트는 상위 2단계
|
|
22
|
+
const distDir = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const pkgRoot = path.resolve(distDir, '..', '..');
|
|
24
|
+
const starterDir = path.join(pkgRoot, 'starter-pack', 'solutions');
|
|
25
|
+
return fs.existsSync(starterDir) ? starterDir : null;
|
|
26
|
+
}
|
|
27
|
+
export function initializeForgenHome(options = {}) {
|
|
28
|
+
const solutionsDir = path.join(ME_DIR, 'solutions');
|
|
29
|
+
const starterDir = findStarterDir();
|
|
30
|
+
if (!starterDir) {
|
|
31
|
+
return {
|
|
32
|
+
solutionsInstalled: 0,
|
|
33
|
+
solutionsSkippedExisting: 0,
|
|
34
|
+
solutionsDir,
|
|
35
|
+
starterDir: null,
|
|
36
|
+
skipped: true,
|
|
37
|
+
skipReason: 'starter-pack directory not found in package',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
let existing = 0;
|
|
41
|
+
if (fs.existsSync(solutionsDir)) {
|
|
42
|
+
existing = fs.readdirSync(solutionsDir).filter((f) => f.endsWith('.md')).length;
|
|
43
|
+
}
|
|
44
|
+
if (existing >= 5 && !options.force) {
|
|
45
|
+
return {
|
|
46
|
+
solutionsInstalled: 0,
|
|
47
|
+
solutionsSkippedExisting: existing,
|
|
48
|
+
solutionsDir,
|
|
49
|
+
starterDir,
|
|
50
|
+
skipped: true,
|
|
51
|
+
skipReason: `${existing} existing solutions (≥5) — use --force to overwrite`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
fs.mkdirSync(solutionsDir, { recursive: true });
|
|
55
|
+
const starterFiles = fs.readdirSync(starterDir).filter((f) => f.endsWith('.md'));
|
|
56
|
+
let installed = 0;
|
|
57
|
+
for (const file of starterFiles) {
|
|
58
|
+
const dest = path.join(solutionsDir, file);
|
|
59
|
+
if (!fs.existsSync(dest) || options.force) {
|
|
60
|
+
fs.cpSync(path.join(starterDir, file), dest);
|
|
61
|
+
installed++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
solutionsInstalled: installed,
|
|
66
|
+
solutionsSkippedExisting: existing,
|
|
67
|
+
solutionsDir,
|
|
68
|
+
starterDir,
|
|
69
|
+
skipped: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export async function handleInit(args) {
|
|
73
|
+
const force = args.includes('--force');
|
|
74
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
75
|
+
console.log(`
|
|
76
|
+
forgen init — starter-pack 프로비저닝 (기존 솔루션 보호)
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
forgen init Install starter-pack if solutions/ has < 5 files
|
|
80
|
+
forgen init --force Overwrite any existing starter files (idempotent)
|
|
81
|
+
FORGEN_HOME=... forgen init 새 홈에 격리 초기화
|
|
82
|
+
|
|
83
|
+
Starter pack = starter-* 로 시작하는 범용 개발 패턴 솔루션. 신규 사용자가
|
|
84
|
+
"compound recall" 효과를 첫날부터 체감할 수 있도록 설치 시 기본 제공되지만,
|
|
85
|
+
npm install-g 을 거치지 않은 격리/컨테이너 환경은 이 CLI 로 수동 배포.
|
|
86
|
+
`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const result = initializeForgenHome({ force });
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(' forgen init');
|
|
92
|
+
console.log(' ──────────');
|
|
93
|
+
console.log(` FORGEN_HOME ${path.dirname(result.solutionsDir)}`);
|
|
94
|
+
console.log(` starter-pack source ${result.starterDir ?? 'NOT FOUND'}`);
|
|
95
|
+
console.log(` existing solutions ${result.solutionsSkippedExisting}`);
|
|
96
|
+
console.log(` newly installed ${result.solutionsInstalled}`);
|
|
97
|
+
if (result.skipped) {
|
|
98
|
+
console.log(` status skipped — ${result.skipReason}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(` status ✓ initialized`);
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
}
|
package/dist/core/init.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { profileExists } from '../store/profile-store.js';
|
|
10
10
|
import { ensureV1Directories } from './v1-bootstrap.js';
|
|
11
|
+
import { initializeForgenHome } from './init-cli.js';
|
|
11
12
|
// ── CLI 핸들러 ──
|
|
12
13
|
export async function handleInit(_args) {
|
|
13
14
|
const cwd = process.cwd();
|
|
@@ -18,6 +19,22 @@ export async function handleInit(_args) {
|
|
|
18
19
|
// 프로젝트 .claude/rules 디렉토리 생성
|
|
19
20
|
const rulesDir = path.join(cwd, '.claude', 'rules');
|
|
20
21
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
22
|
+
// v0.4.1 (2026-04-24): starter-pack 프로비저닝 — 격리 홈 / 신규 FORGEN_HOME
|
|
23
|
+
// 에서 "신규 사용자 첫날 가치" 가 0이 되는 결함 해소. npm install-g 시의
|
|
24
|
+
// postinstall 이 하던 starter 배포를 런타임에서도 보장.
|
|
25
|
+
// 보수적: me/solutions 에 ≥5개면 skip — 기존 사용자 실 축적물 보호.
|
|
26
|
+
try {
|
|
27
|
+
const r = initializeForgenHome();
|
|
28
|
+
if (r.solutionsInstalled > 0) {
|
|
29
|
+
console.log(` ✓ Starter-pack: ${r.solutionsInstalled} solutions installed.`);
|
|
30
|
+
}
|
|
31
|
+
else if (r.skipped && r.solutionsSkippedExisting > 0) {
|
|
32
|
+
console.log(` • Starter-pack: skipped (${r.solutionsSkippedExisting} existing solutions).`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.log(` ⚠ Starter-pack install 실패: ${e.message}`);
|
|
37
|
+
}
|
|
21
38
|
// 프로필 존재 확인
|
|
22
39
|
if (profileExists()) {
|
|
23
40
|
console.log(' Profile already exists. Your personalization is active.');
|
package/dist/core/inspect-cli.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Authoritative: docs/plans/2026-04-03-forgen-rule-renderer-spec.md §6
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
|
-
import * as os from 'node:os';
|
|
9
8
|
import * as path from 'node:path';
|
|
10
9
|
import { loadProfile } from '../store/profile-store.js';
|
|
11
10
|
import { loadAllRules, loadActiveRules } from '../store/rule-store.js';
|
|
@@ -102,7 +101,7 @@ export async function handleInspect(args) {
|
|
|
102
101
|
bypass: 'bypass.jsonl',
|
|
103
102
|
drift: 'drift.jsonl',
|
|
104
103
|
};
|
|
105
|
-
const p = path.join(
|
|
104
|
+
const p = path.join(STATE_DIR, 'enforcement', fileMap[sub]);
|
|
106
105
|
if (!fs.existsSync(p)) {
|
|
107
106
|
console.log(`\n No ${sub} data (${p} not found).\n`);
|
|
108
107
|
return;
|
package/dist/core/installer.js
CHANGED
|
@@ -111,7 +111,7 @@ export function installAgents(cwd, pkgRoot) {
|
|
|
111
111
|
const targetDir = path.join(cwd, '.claude', 'agents');
|
|
112
112
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
113
113
|
const hashes = loadAgentHashes();
|
|
114
|
-
const sourceDir = path.join(pkgRoot, 'agents');
|
|
114
|
+
const sourceDir = path.join(pkgRoot, 'assets', 'claude', 'agents');
|
|
115
115
|
try {
|
|
116
116
|
installAgentsFromDir(sourceDir, targetDir, 'ch-', hashes);
|
|
117
117
|
cleanupStaleAgents(sourceDir, targetDir, 'ch-', hashes);
|
|
@@ -159,7 +159,7 @@ function cleanupStaleCommands(commandsDir, validFiles) {
|
|
|
159
159
|
}
|
|
160
160
|
/** 스킬을 Claude Code 슬래시 명령으로 설치 (패키지 내장만) */
|
|
161
161
|
export function installSlashCommands(_cwd, pkgRoot) {
|
|
162
|
-
let skillsDir = path.join(pkgRoot, 'commands');
|
|
162
|
+
let skillsDir = path.join(pkgRoot, 'assets', 'claude', 'commands');
|
|
163
163
|
if (!fs.existsSync(skillsDir)) {
|
|
164
164
|
skillsDir = path.join(pkgRoot, 'skills');
|
|
165
165
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen migrate` CLI
|
|
3
|
+
*
|
|
4
|
+
* 데이터 스키마 업그레이드를 1회성으로 돌리는 관리 명령.
|
|
5
|
+
* 현재 대상:
|
|
6
|
+
* - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
|
|
7
|
+
* 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
|
|
8
|
+
* 경우 영구 재기록이 필요.
|
|
9
|
+
* - evidence-host: ~/.forgen/me/behavior/*.json 에 host 필드가 없는 파일 백필.
|
|
10
|
+
*/
|
|
11
|
+
export declare function handleMigrate(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen migrate` CLI
|
|
3
|
+
*
|
|
4
|
+
* 데이터 스키마 업그레이드를 1회성으로 돌리는 관리 명령.
|
|
5
|
+
* 현재 대상:
|
|
6
|
+
* - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
|
|
7
|
+
* 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
|
|
8
|
+
* 경우 영구 재기록이 필요.
|
|
9
|
+
* - evidence-host: ~/.forgen/me/behavior/*.json 에 host 필드가 없는 파일 백필.
|
|
10
|
+
*/
|
|
11
|
+
import { migrateImplicitFeedbackLog } from '../store/implicit-feedback-store.js';
|
|
12
|
+
import { migrateEvidenceHost } from './migrate-evidence-host.js';
|
|
13
|
+
const HELP = `
|
|
14
|
+
forgen migrate — one-shot schema migrations
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
forgen migrate implicit-feedback category 필드가 없는 레거시 엔트리 백필 + 재기록
|
|
18
|
+
forgen migrate all (현재는 implicit-feedback 과 동일)
|
|
19
|
+
forgen migrate evidence-host behavior/*.json 에 host 필드 백필
|
|
20
|
+
--dry-run 디스크 미수정, 카운트만 출력
|
|
21
|
+
--default-host <claude|codex> host 기본값 (default: claude)
|
|
22
|
+
forgen migrate --help 이 도움말
|
|
23
|
+
`;
|
|
24
|
+
export async function handleMigrate(args) {
|
|
25
|
+
const sub = args[0];
|
|
26
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
27
|
+
console.log(HELP);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (sub === 'implicit-feedback' || sub === 'all') {
|
|
31
|
+
console.log('[forgen migrate] implicit-feedback.jsonl 백필 시작...');
|
|
32
|
+
const { migrated, dropped } = migrateImplicitFeedbackLog();
|
|
33
|
+
console.log(`[forgen migrate] 백필 ${migrated}건, 드롭 ${dropped}건 — 재기록 완료.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (sub === 'evidence-host') {
|
|
37
|
+
const dryRun = args.includes('--dry-run');
|
|
38
|
+
const hostFlagIdx = args.indexOf('--default-host');
|
|
39
|
+
const rawHost = hostFlagIdx !== -1 ? args[hostFlagIdx + 1] : 'claude';
|
|
40
|
+
if (rawHost !== 'claude' && rawHost !== 'codex') {
|
|
41
|
+
console.error(`[forgen migrate] --default-host 는 'claude' 또는 'codex' 여야 합니다. 받은 값: ${rawHost}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const defaultHost = rawHost;
|
|
45
|
+
const result = migrateEvidenceHost({ defaultHost, dryRun });
|
|
46
|
+
const label = dryRun ? ' (dry-run)' : '';
|
|
47
|
+
console.log(`[forgen] migrated: ${result.migrated} (skipped: ${result.skipped}, total: ${result.total})${label}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.error(`[forgen migrate] unknown target: ${sub}`);
|
|
51
|
+
console.error(HELP);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Evidence Host Backfill Migration
|
|
3
|
+
*
|
|
4
|
+
* spec §10 우선순위 5 + §4.2:
|
|
5
|
+
* 사용자가 명시적으로 디스크의 evidence 파일에 host 필드를 박제하고 싶을 때 사용.
|
|
6
|
+
* (마이그레이션 박제, audit 목적)
|
|
7
|
+
*
|
|
8
|
+
* loadEvidence 의 자동 backfill (evidence-store.ts::backfillHost) 과는 독립.
|
|
9
|
+
* 이 모듈은 디스크 파일을 직접 수정하는 명시적 경로.
|
|
10
|
+
*/
|
|
11
|
+
export interface MigrateEvidenceHostOptions {
|
|
12
|
+
/** backfill 할 host 값 (host 필드 없는 파일에만 적용) */
|
|
13
|
+
defaultHost: 'claude' | 'codex';
|
|
14
|
+
/**
|
|
15
|
+
* true 이면 디스크를 수정하지 않고 카운트만 반환.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface MigrateEvidenceHostResult {
|
|
21
|
+
/** host 필드를 새로 추가한 파일 수 */
|
|
22
|
+
migrated: number;
|
|
23
|
+
/** 처리 대상 전체 .json 파일 수 */
|
|
24
|
+
total: number;
|
|
25
|
+
/** 이미 host 필드가 있어 건너뛴 파일 수 */
|
|
26
|
+
skipped: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `~/.forgen/me/behavior/*.json` 을 순회하여 host 필드가 없는 파일에
|
|
30
|
+
* `defaultHost` 를 추가한다.
|
|
31
|
+
*
|
|
32
|
+
* - dryRun=true 이면 디스크 미수정, 카운트만 반환.
|
|
33
|
+
* - 이미 host 필드가 있는 파일은 건너뜀 (idempotent).
|
|
34
|
+
* - 파싱 실패 / host 필드가 아닌 값인 파일도 건너뜀 (안전).
|
|
35
|
+
*/
|
|
36
|
+
export declare function migrateEvidenceHost(options: MigrateEvidenceHostOptions): MigrateEvidenceHostResult;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Evidence Host Backfill Migration
|
|
3
|
+
*
|
|
4
|
+
* spec §10 우선순위 5 + §4.2:
|
|
5
|
+
* 사용자가 명시적으로 디스크의 evidence 파일에 host 필드를 박제하고 싶을 때 사용.
|
|
6
|
+
* (마이그레이션 박제, audit 목적)
|
|
7
|
+
*
|
|
8
|
+
* loadEvidence 의 자동 backfill (evidence-store.ts::backfillHost) 과는 독립.
|
|
9
|
+
* 이 모듈은 디스크 파일을 직접 수정하는 명시적 경로.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { ME_BEHAVIOR } from './paths.js';
|
|
14
|
+
import { atomicWriteJSON, safeReadJSON } from '../hooks/shared/atomic-write.js';
|
|
15
|
+
/**
|
|
16
|
+
* `~/.forgen/me/behavior/*.json` 을 순회하여 host 필드가 없는 파일에
|
|
17
|
+
* `defaultHost` 를 추가한다.
|
|
18
|
+
*
|
|
19
|
+
* - dryRun=true 이면 디스크 미수정, 카운트만 반환.
|
|
20
|
+
* - 이미 host 필드가 있는 파일은 건너뜀 (idempotent).
|
|
21
|
+
* - 파싱 실패 / host 필드가 아닌 값인 파일도 건너뜀 (안전).
|
|
22
|
+
*/
|
|
23
|
+
export function migrateEvidenceHost(options) {
|
|
24
|
+
const { defaultHost, dryRun = false } = options;
|
|
25
|
+
if (!fs.existsSync(ME_BEHAVIOR)) {
|
|
26
|
+
return { migrated: 0, total: 0, skipped: 0 };
|
|
27
|
+
}
|
|
28
|
+
const files = fs.readdirSync(ME_BEHAVIOR).filter((f) => f.endsWith('.json'));
|
|
29
|
+
let migrated = 0;
|
|
30
|
+
let skipped = 0;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const filePath = path.join(ME_BEHAVIOR, file);
|
|
33
|
+
const data = safeReadJSON(filePath, null);
|
|
34
|
+
if (data === null || typeof data !== 'object') {
|
|
35
|
+
skipped++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const host = data['host'];
|
|
39
|
+
if (host === 'claude' || host === 'codex') {
|
|
40
|
+
skipped++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!dryRun) {
|
|
44
|
+
atomicWriteJSON(filePath, { ...data, host: defaultHost }, { pretty: true });
|
|
45
|
+
}
|
|
46
|
+
migrated++;
|
|
47
|
+
}
|
|
48
|
+
return { migrated, total: files.length, skipped };
|
|
49
|
+
}
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
export declare const CLAUDE_DIR: string;
|
|
3
3
|
/** ~/.claude/settings.json — Claude Code 설정 파일 */
|
|
4
4
|
export declare const SETTINGS_PATH: string;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* ~/.forgen/ — v1 하네스 홈 디렉토리.
|
|
7
|
+
*
|
|
8
|
+
* v0.4.1 (2026-04-24): FORGEN_HOME env 로 override 가능.
|
|
9
|
+
* 목적: CI/e2e 에서 격리된 fresh forgen 홈으로 "신규 사용자 시뮬" + 내 실 홈
|
|
10
|
+
* (2338+ 세션 축적물) 을 건드리지 않음. README/docs 에도 "테스트 격리" 섹션
|
|
11
|
+
* 으로 노출 예정.
|
|
12
|
+
*/
|
|
6
13
|
export declare const FORGEN_HOME: string;
|
|
7
14
|
/** ~/.forgen/me/ — 개인 공간 (v5.1: ~/.compound/ → ~/.forgen/ 통합) */
|
|
8
15
|
export declare const ME_DIR: string;
|
package/dist/core/paths.js
CHANGED
|
@@ -5,8 +5,17 @@ const HOME = os.homedir();
|
|
|
5
5
|
export const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
6
6
|
/** ~/.claude/settings.json — Claude Code 설정 파일 */
|
|
7
7
|
export const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* ~/.forgen/ — v1 하네스 홈 디렉토리.
|
|
10
|
+
*
|
|
11
|
+
* v0.4.1 (2026-04-24): FORGEN_HOME env 로 override 가능.
|
|
12
|
+
* 목적: CI/e2e 에서 격리된 fresh forgen 홈으로 "신규 사용자 시뮬" + 내 실 홈
|
|
13
|
+
* (2338+ 세션 축적물) 을 건드리지 않음. README/docs 에도 "테스트 격리" 섹션
|
|
14
|
+
* 으로 노출 예정.
|
|
15
|
+
*/
|
|
16
|
+
export const FORGEN_HOME = process.env.FORGEN_HOME
|
|
17
|
+
? path.resolve(process.env.FORGEN_HOME)
|
|
18
|
+
: path.join(HOME, '.forgen');
|
|
10
19
|
/** ~/.forgen/me/ — 개인 공간 (v5.1: ~/.compound/ → ~/.forgen/ 통합) */
|
|
11
20
|
export const ME_DIR = path.join(FORGEN_HOME, 'me');
|
|
12
21
|
/** ~/.forgen/me/philosophy.json — 개인 철학 */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen recall` CLI (H5)
|
|
3
|
+
*
|
|
4
|
+
* 최근 UserPromptSubmit 에서 매칭/surface 된 솔루션을 사용자에게 되짚어주는 명령.
|
|
5
|
+
*
|
|
6
|
+
* 목적: v0.4.0 에서 compound 솔루션이 8,000+ 번 recall 되었지만 사용자는 0건을
|
|
7
|
+
* 확인할 수 없었다. 이 CLI 는 `~/.forgen/state/implicit-feedback.jsonl` 과
|
|
8
|
+
* `~/.forgen/state/match-eval-log.jsonl` 을 읽어 "최근 어떤 지식이 붙었나" 를
|
|
9
|
+
* 1초 안에 보여준다. `--show` 플래그로 솔루션 본문 preview 까지.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* forgen recall 최근 10건 요약
|
|
13
|
+
* forgen recall --limit 20 최근 N건
|
|
14
|
+
* forgen recall --show 본문 preview 포함
|
|
15
|
+
* forgen recall --json JSON 출력 (script 연동용)
|
|
16
|
+
*/
|
|
17
|
+
interface RecallEntry {
|
|
18
|
+
at: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
solution: string;
|
|
21
|
+
match_score?: number;
|
|
22
|
+
}
|
|
23
|
+
/** H5: implicit-feedback.jsonl 에서 recommendation_surfaced 만 시간역순으로 추출. */
|
|
24
|
+
export declare function loadRecentRecalls(limit?: number): RecallEntry[];
|
|
25
|
+
export declare function handleRecall(args: string[]): Promise<void>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen recall` CLI (H5)
|
|
3
|
+
*
|
|
4
|
+
* 최근 UserPromptSubmit 에서 매칭/surface 된 솔루션을 사용자에게 되짚어주는 명령.
|
|
5
|
+
*
|
|
6
|
+
* 목적: v0.4.0 에서 compound 솔루션이 8,000+ 번 recall 되었지만 사용자는 0건을
|
|
7
|
+
* 확인할 수 없었다. 이 CLI 는 `~/.forgen/state/implicit-feedback.jsonl` 과
|
|
8
|
+
* `~/.forgen/state/match-eval-log.jsonl` 을 읽어 "최근 어떤 지식이 붙었나" 를
|
|
9
|
+
* 1초 안에 보여준다. `--show` 플래그로 솔루션 본문 preview 까지.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* forgen recall 최근 10건 요약
|
|
13
|
+
* forgen recall --limit 20 최근 N건
|
|
14
|
+
* forgen recall --show 본문 preview 포함
|
|
15
|
+
* forgen recall --json JSON 출력 (script 연동용)
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { STATE_DIR, ME_SOLUTIONS } from './paths.js';
|
|
20
|
+
function readJsonl(p) {
|
|
21
|
+
if (!fs.existsSync(p))
|
|
22
|
+
return [];
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
25
|
+
if (!line.trim())
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
out.push(JSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
catch { /* skip malformed */ }
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** H5: implicit-feedback.jsonl 에서 recommendation_surfaced 만 시간역순으로 추출. */
|
|
35
|
+
export function loadRecentRecalls(limit = 10) {
|
|
36
|
+
const entries = readJsonl(path.join(STATE_DIR, 'implicit-feedback.jsonl'));
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (e.type !== 'recommendation_surfaced')
|
|
40
|
+
continue;
|
|
41
|
+
if (typeof e.at !== 'string' || typeof e.solution !== 'string')
|
|
42
|
+
continue;
|
|
43
|
+
out.push({
|
|
44
|
+
at: e.at,
|
|
45
|
+
sessionId: typeof e.sessionId === 'string' ? e.sessionId : 'unknown',
|
|
46
|
+
solution: e.solution,
|
|
47
|
+
match_score: typeof e.match_score === 'number' ? e.match_score : undefined,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return out.sort((a, b) => b.at.localeCompare(a.at)).slice(0, limit);
|
|
51
|
+
}
|
|
52
|
+
/** 솔루션 body preview — frontmatter 뒤 첫 N줄. */
|
|
53
|
+
function readSolutionPreview(solutionName, maxLines = 8) {
|
|
54
|
+
const candidates = [
|
|
55
|
+
path.join(ME_SOLUTIONS, `${solutionName}.md`),
|
|
56
|
+
path.join(ME_SOLUTIONS, solutionName),
|
|
57
|
+
];
|
|
58
|
+
for (const p of candidates) {
|
|
59
|
+
if (!fs.existsSync(p))
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
63
|
+
// frontmatter block skip (--- ... ---)
|
|
64
|
+
const stripped = raw.replace(/^---[\s\S]*?---\n?/, '');
|
|
65
|
+
const lines = stripped.split('\n').filter((l) => l.length > 0).slice(0, maxLines);
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function parseArgs(args) {
|
|
75
|
+
let limit = 10;
|
|
76
|
+
let showBody = false;
|
|
77
|
+
let json = false;
|
|
78
|
+
for (let i = 0; i < args.length; i++) {
|
|
79
|
+
const a = args[i];
|
|
80
|
+
if (a === '--limit' && i + 1 < args.length) {
|
|
81
|
+
const n = Number(args[++i]);
|
|
82
|
+
if (Number.isFinite(n) && n > 0)
|
|
83
|
+
limit = Math.min(100, Math.floor(n));
|
|
84
|
+
}
|
|
85
|
+
else if (a === '--show' || a === '--body') {
|
|
86
|
+
showBody = true;
|
|
87
|
+
}
|
|
88
|
+
else if (a === '--json') {
|
|
89
|
+
json = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { limit, showBody, json };
|
|
93
|
+
}
|
|
94
|
+
export async function handleRecall(args) {
|
|
95
|
+
const { limit, showBody, json } = parseArgs(args);
|
|
96
|
+
const recalls = loadRecentRecalls(limit);
|
|
97
|
+
if (json) {
|
|
98
|
+
const payload = recalls.map((r) => ({
|
|
99
|
+
...r,
|
|
100
|
+
preview: showBody ? readSolutionPreview(r.solution) : undefined,
|
|
101
|
+
}));
|
|
102
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (recalls.length === 0) {
|
|
106
|
+
console.log(' (no recent recalls — run a session with compound hooks enabled)');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(` forgen recall — last ${recalls.length} surfaced solution${recalls.length === 1 ? '' : 's'}`);
|
|
111
|
+
console.log(' ─────────────────────────────────────────');
|
|
112
|
+
for (const r of recalls) {
|
|
113
|
+
const score = r.match_score !== undefined ? ` @${r.match_score.toFixed(2)}` : '';
|
|
114
|
+
console.log(` ${r.at.slice(0, 19).replace('T', ' ')} ${r.solution}${score}`);
|
|
115
|
+
if (showBody) {
|
|
116
|
+
const body = readSolutionPreview(r.solution);
|
|
117
|
+
if (body) {
|
|
118
|
+
for (const line of body.split('\n'))
|
|
119
|
+
console.log(` │ ${line}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — Recall Reference Detector (H4 완결)
|
|
3
|
+
*
|
|
4
|
+
* US-06 에서 `recommendation_surfaced` (주입 = Claude 컨텍스트에 보여졌다) 는
|
|
5
|
+
* emit 경로가 있지만, `recall_referenced` (Claude 가 실제로 참조/인용했다) 는
|
|
6
|
+
* enum 만 정의되고 emit 경로가 없었다. 이 결함 때문에 "채널은 뚫렸지만 활용은
|
|
7
|
+
* 측정 불가" 상태. 이 모듈이 그 측정 경로를 닫는다.
|
|
8
|
+
*
|
|
9
|
+
* 알고리즘:
|
|
10
|
+
* 1. Stop hook 에서 lastAssistantMessage 를 읽는다.
|
|
11
|
+
* 2. 현재 세션의 injection-cache 에서 최근 주입된 솔루션 목록을 가져온다.
|
|
12
|
+
* 3. 각 솔루션의 **name** 이 메시지 텍스트에 등장하면 참조한 것으로 간주.
|
|
13
|
+
* (tag 매칭은 false-positive 과다 — "협업" 같은 흔한 단어로 오매칭.)
|
|
14
|
+
* 4. 중복 emit 방지: injection-cache 엔트리에 `_referenced: true` 플래그 세팅.
|
|
15
|
+
*
|
|
16
|
+
* 순수 함수 설계 — Stop hook 이 inject-cache 를 읽고 쓰는 I/O 는 호출지에서.
|
|
17
|
+
*/
|
|
18
|
+
export interface InjectedSolutionEntry {
|
|
19
|
+
name: string;
|
|
20
|
+
identifiers?: string[];
|
|
21
|
+
tags?: string[];
|
|
22
|
+
status?: string;
|
|
23
|
+
injectedAt?: string;
|
|
24
|
+
_referenced?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface RecallReferenceDetection {
|
|
27
|
+
/** 이번 턴에 처음 참조가 감지된 솔루션 이름 목록. */
|
|
28
|
+
newlyReferenced: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 순수 판정 — text 안에 아직 참조 안 된 솔루션의 name / 고유 식별자 / 희귀 태그
|
|
32
|
+
* 조합이 등장하면 수집.
|
|
33
|
+
*
|
|
34
|
+
* v0.4.1 초기: name (slug kebab-case) literal 만 매칭 → Claude 가 content 만 인용
|
|
35
|
+
* 하고 slug 를 안 쓰면 측정 불가.
|
|
36
|
+
* v0.4.1 확장 (2026-04-24): identifier (함수/파일명 literal, >=4자) 또는 **복합
|
|
37
|
+
* 태그 2개 동시 등장** 도 weak reference 로 인정. false-positive 완화 위해:
|
|
38
|
+
* - identifier 는 길이 ≥4
|
|
39
|
+
* - tag 는 **복합 슬러그 (`-` 또는 `_` 포함)** 만 허용 + length ≥6
|
|
40
|
+
* → "tdd", "test", "workflow" 같은 일반 태그 단독 매칭은 제외
|
|
41
|
+
* - tag 매칭은 최소 2개 교차
|
|
42
|
+
*/
|
|
43
|
+
export declare function detectRecallReferences(text: string, injected: readonly InjectedSolutionEntry[]): RecallReferenceDetection;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — Recall Reference Detector (H4 완결)
|
|
3
|
+
*
|
|
4
|
+
* US-06 에서 `recommendation_surfaced` (주입 = Claude 컨텍스트에 보여졌다) 는
|
|
5
|
+
* emit 경로가 있지만, `recall_referenced` (Claude 가 실제로 참조/인용했다) 는
|
|
6
|
+
* enum 만 정의되고 emit 경로가 없었다. 이 결함 때문에 "채널은 뚫렸지만 활용은
|
|
7
|
+
* 측정 불가" 상태. 이 모듈이 그 측정 경로를 닫는다.
|
|
8
|
+
*
|
|
9
|
+
* 알고리즘:
|
|
10
|
+
* 1. Stop hook 에서 lastAssistantMessage 를 읽는다.
|
|
11
|
+
* 2. 현재 세션의 injection-cache 에서 최근 주입된 솔루션 목록을 가져온다.
|
|
12
|
+
* 3. 각 솔루션의 **name** 이 메시지 텍스트에 등장하면 참조한 것으로 간주.
|
|
13
|
+
* (tag 매칭은 false-positive 과다 — "협업" 같은 흔한 단어로 오매칭.)
|
|
14
|
+
* 4. 중복 emit 방지: injection-cache 엔트리에 `_referenced: true` 플래그 세팅.
|
|
15
|
+
*
|
|
16
|
+
* 순수 함수 설계 — Stop hook 이 inject-cache 를 읽고 쓰는 I/O 는 호출지에서.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* 순수 판정 — text 안에 아직 참조 안 된 솔루션의 name / 고유 식별자 / 희귀 태그
|
|
20
|
+
* 조합이 등장하면 수집.
|
|
21
|
+
*
|
|
22
|
+
* v0.4.1 초기: name (slug kebab-case) literal 만 매칭 → Claude 가 content 만 인용
|
|
23
|
+
* 하고 slug 를 안 쓰면 측정 불가.
|
|
24
|
+
* v0.4.1 확장 (2026-04-24): identifier (함수/파일명 literal, >=4자) 또는 **복합
|
|
25
|
+
* 태그 2개 동시 등장** 도 weak reference 로 인정. false-positive 완화 위해:
|
|
26
|
+
* - identifier 는 길이 ≥4
|
|
27
|
+
* - tag 는 **복합 슬러그 (`-` 또는 `_` 포함)** 만 허용 + length ≥6
|
|
28
|
+
* → "tdd", "test", "workflow" 같은 일반 태그 단독 매칭은 제외
|
|
29
|
+
* - tag 매칭은 최소 2개 교차
|
|
30
|
+
*/
|
|
31
|
+
export function detectRecallReferences(text, injected) {
|
|
32
|
+
if (!text || injected.length === 0)
|
|
33
|
+
return { newlyReferenced: [] };
|
|
34
|
+
const newlyReferenced = [];
|
|
35
|
+
for (const sol of injected) {
|
|
36
|
+
if (sol._referenced)
|
|
37
|
+
continue;
|
|
38
|
+
if (!sol.name || sol.name.length < 4)
|
|
39
|
+
continue;
|
|
40
|
+
let matched = false;
|
|
41
|
+
// 1순위: slug name 정확 매칭 (precision 최고)
|
|
42
|
+
if (text.includes(sol.name)) {
|
|
43
|
+
matched = true;
|
|
44
|
+
}
|
|
45
|
+
// 2순위: 고유 identifier (함수/파일명 literal) 매칭
|
|
46
|
+
if (!matched && sol.identifiers) {
|
|
47
|
+
for (const id of sol.identifiers) {
|
|
48
|
+
if (typeof id === 'string' && id.length >= 4 && text.includes(id)) {
|
|
49
|
+
matched = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 3순위: 복합-슬러그 태그 2개 이상 동시 등장 (일반 단어 단독은 제외)
|
|
55
|
+
if (!matched && sol.tags) {
|
|
56
|
+
const specificTags = sol.tags.filter((t) => typeof t === 'string' && t.length >= 6 && (t.includes('-') || t.includes('_')));
|
|
57
|
+
const hits = specificTags.filter((t) => text.includes(t));
|
|
58
|
+
if (hits.length >= 2)
|
|
59
|
+
matched = true;
|
|
60
|
+
}
|
|
61
|
+
if (matched)
|
|
62
|
+
newlyReferenced.push(sol.name);
|
|
63
|
+
}
|
|
64
|
+
return { newlyReferenced };
|
|
65
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { generateHooksJson } from '../hooks/hooks-generator.js';
|
|
10
|
+
import { getHostRuntime } from '../host/host-runtime.js';
|
|
10
11
|
import { ConfigError } from './errors.js';
|
|
11
12
|
import { createLogger } from './logger.js';
|
|
12
13
|
import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, readSettingsSafely, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
|
|
@@ -74,7 +75,8 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
|
|
|
74
75
|
hooksConfig[event] = filtered;
|
|
75
76
|
}
|
|
76
77
|
try {
|
|
77
|
-
|
|
78
|
+
const host = getHostRuntime(runtime);
|
|
79
|
+
if (host.hookInjectionStrategy === 'generate') {
|
|
78
80
|
const generated = generateHooksJson({ cwd, runtime, pluginRoot: path.join(pkgRoot, 'dist') });
|
|
79
81
|
for (const [event, handlers] of Object.entries(generated.hooks)) {
|
|
80
82
|
if (!hooksConfig[event])
|
|
@@ -83,7 +85,7 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
else {
|
|
86
|
-
//
|
|
88
|
+
// 'pre-baked-file': pkgRoot/hooks/hooks.json 읽고 ${CLAUDE_PLUGIN_ROOT} 치환
|
|
87
89
|
const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
|
|
88
90
|
if (fs.existsSync(hooksJsonPath)) {
|
|
89
91
|
const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { V1HarnessContext } from './harness.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { RuntimeHost } from './types.js';
|
|
3
3
|
/** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
|
|
4
4
|
export declare function spawnClaude(args: string[], context: V1HarnessContext, runtime?: RuntimeHost): Promise<number>;
|
|
5
5
|
/**
|