@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,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectToClaudeEvent — Multi-Host Core Design §5.2 / §10 우선순위 2
|
|
3
|
+
*
|
|
4
|
+
* Codex (또는 미래의 다른 host) 의 hook 출력을 Claude Hook schema 로 사영하는
|
|
5
|
+
* 정식 계약. spec §17.4 / §18.4 에서 검증되었듯 schema-level 에서 거의 identity 이므로
|
|
6
|
+
* 본 함수는 *형식 정규화* 만 책임진다.
|
|
7
|
+
*
|
|
8
|
+
* - 입력: host-native 출력(JSON object, plaintext, exit-code 등은 별도 layer 에서 처리)
|
|
9
|
+
* - 출력: Claude HookEventOutput 동치 — `continue`, `hookSpecificOutput.permissionDecision`, etc.
|
|
10
|
+
* - 실패 정책: parse 실패 / 알 수 없는 형식 → fail-open (`{ continue: true }`)
|
|
11
|
+
*
|
|
12
|
+
* 본 모듈은 host 측 표면을 *모르고*, 받은 raw 의 형태만으로 동작한다 (1원칙: core 는 Claude
|
|
13
|
+
* semantics 알아도 됨, Codex 표면 모름). 즉 Codex CLI 의 stdout 을 받아 코어가 학습 가능한
|
|
14
|
+
* Claude 형 객체로 변환만 한다.
|
|
15
|
+
*/
|
|
16
|
+
function parseDecision(raw) {
|
|
17
|
+
if (typeof raw === 'boolean')
|
|
18
|
+
return { continueFlag: raw };
|
|
19
|
+
if (typeof raw === 'string') {
|
|
20
|
+
const normalized = raw.toLowerCase();
|
|
21
|
+
if (normalized === 'continue')
|
|
22
|
+
return { continueFlag: true };
|
|
23
|
+
if (normalized === 'stop' ||
|
|
24
|
+
normalized === 'deny' ||
|
|
25
|
+
normalized === 'reject' ||
|
|
26
|
+
normalized === 'block') {
|
|
27
|
+
return { continueFlag: false, permissionDecision: normalized };
|
|
28
|
+
}
|
|
29
|
+
return { continueFlag: true };
|
|
30
|
+
}
|
|
31
|
+
if (typeof raw !== 'object' || raw === null)
|
|
32
|
+
return { continueFlag: true };
|
|
33
|
+
const decision = raw.decision;
|
|
34
|
+
if (typeof decision === 'string') {
|
|
35
|
+
const normalized = decision.toLowerCase();
|
|
36
|
+
if (normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
|
|
37
|
+
return { continueFlag: false, permissionDecision: normalized };
|
|
38
|
+
}
|
|
39
|
+
if (normalized === 'ask' || normalized === 'prompt' || normalized === 'confirm') {
|
|
40
|
+
return { continueFlag: true, permissionDecision: normalized };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (typeof raw.approved === 'boolean') {
|
|
44
|
+
const approved = raw.approved;
|
|
45
|
+
return approved
|
|
46
|
+
? { continueFlag: true, permissionDecision: raw.decision || 'approve' }
|
|
47
|
+
: { continueFlag: false, permissionDecision: 'deny' };
|
|
48
|
+
}
|
|
49
|
+
if (typeof raw.continue === 'boolean') {
|
|
50
|
+
return { continueFlag: raw.continue };
|
|
51
|
+
}
|
|
52
|
+
return { continueFlag: true };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Codex 출력 → Claude HookEventOutput 정식 사영.
|
|
56
|
+
*
|
|
57
|
+
* spec §18.2 fact #3 에 따라 PreToolUse 의 *이중* decision 필드 중 어댑터는
|
|
58
|
+
* `hookSpecificOutput.permissionDecision` 을 우선한다. 본 함수가 그 규약을 강제.
|
|
59
|
+
*/
|
|
60
|
+
export const projectCodexToClaude = (raw, input) => {
|
|
61
|
+
const result = { continue: true };
|
|
62
|
+
const decision = parseDecision(raw);
|
|
63
|
+
result.continue = decision.continueFlag;
|
|
64
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
65
|
+
const payload = raw;
|
|
66
|
+
if (typeof payload.continue === 'boolean')
|
|
67
|
+
result.continue = payload.continue;
|
|
68
|
+
if (typeof payload.systemMessage === 'string')
|
|
69
|
+
result.systemMessage = payload.systemMessage;
|
|
70
|
+
if (typeof payload.suppressOutput === 'boolean')
|
|
71
|
+
result.suppressOutput = payload.suppressOutput;
|
|
72
|
+
if (typeof payload.hookSpecificOutput === 'object' && payload.hookSpecificOutput !== null) {
|
|
73
|
+
result.hookSpecificOutput = { ...payload.hookSpecificOutput };
|
|
74
|
+
}
|
|
75
|
+
// top-level decision (Codex 의 PreToolUse 이중 decision 중 legacy 측 또는 Stop/Post 의 단일 측)
|
|
76
|
+
// 이 있고, hookSpecificOutput.permissionDecision 이 비었을 때만 보존.
|
|
77
|
+
if (typeof payload.decision === 'string' &&
|
|
78
|
+
!(result.hookSpecificOutput && 'permissionDecision' in result.hookSpecificOutput)) {
|
|
79
|
+
result.hookSpecificOutput = {
|
|
80
|
+
...(result.hookSpecificOutput ?? {}),
|
|
81
|
+
permissionDecision: payload.decision,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const eventName = result.hookSpecificOutput?.hookEventName ?? input.hookEventName ?? input.event;
|
|
86
|
+
if (eventName) {
|
|
87
|
+
result.hookSpecificOutput = {
|
|
88
|
+
hookEventName: eventName,
|
|
89
|
+
...(result.hookSpecificOutput ?? {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!result.continue && !result.hookSpecificOutput?.permissionDecision) {
|
|
93
|
+
if (decision.permissionDecision) {
|
|
94
|
+
result.hookSpecificOutput = {
|
|
95
|
+
...(result.hookSpecificOutput ?? {}),
|
|
96
|
+
permissionDecision: decision.permissionDecision,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
result.hookSpecificOutput = {
|
|
101
|
+
...(result.hookSpecificOutput ?? {}),
|
|
102
|
+
permissionDecision: 'deny',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Claude 어댑터의 사영. 1원칙(Claude reference) + spec §18.4 (Codex hooks.json schema 동일성)
|
|
110
|
+
* 에 따라 본 함수는 `projectCodexToClaude` 와 *같은 normalize 로직* 을 공유한다.
|
|
111
|
+
* 둘 다 같은 canonical Claude HookEventOutput 형식을 만든다.
|
|
112
|
+
*
|
|
113
|
+
* (왜 두 함수를 별도 export 하는가: 향후 schema 가 다른 host 가 추가될 때 본 binding 만
|
|
114
|
+
* 교체하면 되도록 — `getProjection(host)` 가 단일 진입점.)
|
|
115
|
+
*/
|
|
116
|
+
export const projectClaudeToClaude = (raw, input) => projectCodexToClaude(raw, input);
|
|
117
|
+
const PROJECTIONS = {
|
|
118
|
+
claude: projectClaudeToClaude,
|
|
119
|
+
codex: projectCodexToClaude,
|
|
120
|
+
};
|
|
121
|
+
export function getProjection(host) {
|
|
122
|
+
const fn = PROJECTIONS[host];
|
|
123
|
+
if (!fn)
|
|
124
|
+
throw new Error(`No ProjectToClaudeEvent registered for host: ${host}`);
|
|
125
|
+
return fn;
|
|
126
|
+
}
|
package/dist/i18n/index.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* 사용자 대면 출력만 로케일에 따라 전환.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
|
-
import
|
|
9
|
-
import * as os from 'node:os';
|
|
8
|
+
import { GLOBAL_CONFIG } from '../core/paths.js';
|
|
10
9
|
// ── Pack Display Names ──
|
|
11
10
|
const QUALITY_NAMES = {
|
|
12
11
|
ko: { '보수형': '보수형', '균형형': '균형형', '속도형': '속도형' },
|
|
@@ -215,10 +214,9 @@ export function getLocale() { return _currentLocale; }
|
|
|
215
214
|
/** GlobalConfig에서 locale을 읽어 설정. 없으면 'en' 기본값. */
|
|
216
215
|
export function initLocaleFromConfig() {
|
|
217
216
|
try {
|
|
218
|
-
|
|
219
|
-
if (!fs.existsSync(configPath))
|
|
217
|
+
if (!fs.existsSync(GLOBAL_CONFIG))
|
|
220
218
|
return;
|
|
221
|
-
const config = JSON.parse(fs.readFileSync(
|
|
219
|
+
const config = JSON.parse(fs.readFileSync(GLOBAL_CONFIG, 'utf-8'));
|
|
222
220
|
if (config.locale === 'ko' || config.locale === 'en') {
|
|
223
221
|
_currentLocale = config.locale;
|
|
224
222
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -12,6 +12,17 @@ if (!process.env.FORGEN_CWD && !process.env.COMPOUND_CWD) {
|
|
|
12
12
|
process.env.FORGEN_CWD = process.cwd();
|
|
13
13
|
process.env.COMPOUND_CWD = process.cwd(); // legacy compat
|
|
14
14
|
}
|
|
15
|
+
// Multi-host evidence attribution (spec §10-5):
|
|
16
|
+
// 호출 host 가 spawn 시 `--host=<claude|codex>` 인자를 넘기면 본 process 의
|
|
17
|
+
// FORGEN_HOST env 를 set 한다. evidence-store 의 detectHost() 가 이 env 를 읽어
|
|
18
|
+
// correction-record 가 정확한 host 로 박제되게 한다. 미지정 시 기존 fallback
|
|
19
|
+
// (FORGEN_HOST > CODEX_HOME 추론 > 'claude') 그대로.
|
|
20
|
+
const hostArg = process.argv
|
|
21
|
+
.find((a) => a === '--host=claude' || a === '--host=codex')
|
|
22
|
+
?.split('=')[1];
|
|
23
|
+
if (hostArg === 'claude' || hostArg === 'codex') {
|
|
24
|
+
process.env.FORGEN_HOST = hostArg;
|
|
25
|
+
}
|
|
15
26
|
const INSTRUCTIONS = [
|
|
16
27
|
'Forgen compound knowledge — accumulated patterns and solutions from past sessions.',
|
|
17
28
|
'',
|
package/dist/mcp/tools.js
CHANGED
|
@@ -441,4 +441,51 @@ export function registerTools(server) {
|
|
|
441
441
|
}],
|
|
442
442
|
};
|
|
443
443
|
});
|
|
444
|
+
// ── invoke-agent (P3-4/P3-5) ─────────────────────────────────────────
|
|
445
|
+
//
|
|
446
|
+
// forgen 의 13 sub-agents (assets/claude/agents/*.md) 를 MCP 도구로 노출.
|
|
447
|
+
// Claude 의 Task tool 동치를 host 무관 형태로 제공:
|
|
448
|
+
// - Claude main agent: Task tool 우선 사용 (native, faster)
|
|
449
|
+
// - Codex main agent: invoke-agent MCP 도구가 codex exec --json 으로 child spawn
|
|
450
|
+
// 실 작업: assets/claude/agents/<name>.md 의 system prompt 를 prefix 로,
|
|
451
|
+
// 사용자 task 를 prompt 로 child host CLI 호출 → 응답 반환.
|
|
452
|
+
server.registerTool('invoke-agent', {
|
|
453
|
+
description: [
|
|
454
|
+
'Invoke a forgen sub-agent (specialized prompt) on a task and return its summary.',
|
|
455
|
+
'Use when delegation is helpful — e.g., focused exploration (ch-explore), implementation (ch-executor),',
|
|
456
|
+
'critical review (ch-critic), test writing (ch-test-engineer).',
|
|
457
|
+
'The sub-agent runs in a separate child process with isolated context and returns just the result.',
|
|
458
|
+
'Available agent names match files under assets/claude/agents/ (e.g., ch-explore, ch-executor, ch-critic).',
|
|
459
|
+
].join('\n'),
|
|
460
|
+
inputSchema: {
|
|
461
|
+
agent_name: z.string().describe('Sub-agent identifier (e.g., "ch-explore", "ch-executor")'),
|
|
462
|
+
task: z.string().describe('What you want the sub-agent to do — natural language task description'),
|
|
463
|
+
timeout_ms: z.number().int().positive().max(300000).optional()
|
|
464
|
+
.describe('Child timeout in ms (default 60s, max 5m)'),
|
|
465
|
+
},
|
|
466
|
+
annotations: { readOnlyHint: false },
|
|
467
|
+
}, async ({ agent_name, task, timeout_ms }) => {
|
|
468
|
+
try {
|
|
469
|
+
const { invokeAgent } = await import('../host/invoke-agent.js');
|
|
470
|
+
const result = await invokeAgent({ agentName: agent_name, task, timeoutMs: timeout_ms });
|
|
471
|
+
const lines = [
|
|
472
|
+
`[invoke-agent] ${agent_name} (${result.host}, ${result.durationMs}ms)`,
|
|
473
|
+
'',
|
|
474
|
+
result.summary,
|
|
475
|
+
];
|
|
476
|
+
if (result.usage) {
|
|
477
|
+
lines.push('', `Usage: input=${result.usage.input_tokens ?? '?'} output=${result.usage.output_tokens ?? '?'}`);
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
485
|
+
return {
|
|
486
|
+
content: [{ type: 'text', text: `[invoke-agent] error: ${msg}` }],
|
|
487
|
+
isError: true,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
});
|
|
444
491
|
}
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
* - launch context(런타임 + 정제된 args)를 단일 타입으로 통일
|
|
11
11
|
* - CLI/fgx에서 수집한 런타임 값을 Harness, Spawn, Hook Generator에 일관되게 전달
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import type { LaunchContext } from '../core/types.js';
|
|
14
14
|
/**
|
|
15
15
|
* CLI 인자를 파싱해 런타임 결정 + 런타임 플래그 제거
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* 우선순위 (높음→낮음):
|
|
17
|
+
* 1. --runtime <claude|codex> flag
|
|
18
|
+
* 2. FORGEN_RUNTIME env
|
|
19
|
+
* 3. profile.default_host (P1-4)
|
|
20
|
+
* 4. 'claude' fallback (legacy 호환)
|
|
18
21
|
*/
|
|
19
22
|
export declare function resolveLaunchContext(args: string[]): LaunchContext;
|
package/dist/services/session.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* - launch context(런타임 + 정제된 args)를 단일 타입으로 통일
|
|
11
11
|
* - CLI/fgx에서 수집한 런타임 값을 Harness, Spawn, Hook Generator에 일관되게 전달
|
|
12
12
|
*/
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
const localRequire = createRequire(import.meta.url);
|
|
13
15
|
/** 런타임 정규화: 외부 문자열을 내부 enum으로 변환 */
|
|
14
16
|
function parseRuntime(raw) {
|
|
15
17
|
if (!raw)
|
|
@@ -24,17 +26,44 @@ function parseRuntime(raw) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
const DEFAULT_RUNTIME = 'claude';
|
|
29
|
+
/**
|
|
30
|
+
* profile.default_host 를 읽어 runtime 결정.
|
|
31
|
+
* 'ask' 면 별도 prompt 책임 — 본 함수는 default 'claude' 로 fallback (caller 가 --ask 처리).
|
|
32
|
+
* profile-store import 가 cycle 위험이라 require 로 lazy.
|
|
33
|
+
*/
|
|
34
|
+
function readProfileDefaultRuntime() {
|
|
35
|
+
try {
|
|
36
|
+
const mod = localRequire('../store/profile-store.js');
|
|
37
|
+
const stored = mod.getDefaultHost?.();
|
|
38
|
+
if (stored === 'claude' || stored === 'codex')
|
|
39
|
+
return stored;
|
|
40
|
+
return null; // 'ask' 또는 미설정
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
27
46
|
/**
|
|
28
47
|
* CLI 인자를 파싱해 런타임 결정 + 런타임 플래그 제거
|
|
29
|
-
*
|
|
30
|
-
*
|
|
48
|
+
* 우선순위 (높음→낮음):
|
|
49
|
+
* 1. --runtime <claude|codex> flag
|
|
50
|
+
* 2. FORGEN_RUNTIME env
|
|
51
|
+
* 3. profile.default_host (P1-4)
|
|
52
|
+
* 4. 'claude' fallback (legacy 호환)
|
|
31
53
|
*/
|
|
32
54
|
export function resolveLaunchContext(args) {
|
|
33
55
|
const runtimeFromEnv = parseRuntime(process.env.FORGEN_RUNTIME);
|
|
56
|
+
const runtimeFromProfile = runtimeFromEnv ? null : readProfileDefaultRuntime();
|
|
57
|
+
const initial = runtimeFromEnv ?? runtimeFromProfile ?? DEFAULT_RUNTIME;
|
|
58
|
+
const initialSource = runtimeFromEnv
|
|
59
|
+
? 'env'
|
|
60
|
+
: runtimeFromProfile
|
|
61
|
+
? 'profile'
|
|
62
|
+
: 'default';
|
|
34
63
|
const result = {
|
|
35
|
-
runtime:
|
|
64
|
+
runtime: initial,
|
|
36
65
|
args: [],
|
|
37
|
-
runtimeSource:
|
|
66
|
+
runtimeSource: initialSource,
|
|
38
67
|
};
|
|
39
68
|
for (let i = 0; i < args.length; i += 1) {
|
|
40
69
|
const arg = args[i];
|
|
@@ -14,6 +14,7 @@ export declare function createEvidence(params: {
|
|
|
14
14
|
candidate_rule_refs?: string[];
|
|
15
15
|
confidence: number;
|
|
16
16
|
raw_payload?: Record<string, unknown>;
|
|
17
|
+
host?: 'claude' | 'codex';
|
|
17
18
|
}): Evidence;
|
|
18
19
|
export declare function saveEvidence(evidence: Evidence): void;
|
|
19
20
|
/**
|
|
@@ -17,6 +17,24 @@ import { appendLifecycleEvents } from '../engine/lifecycle/meta-reclassifier.js'
|
|
|
17
17
|
function evidencePath(evidenceId) {
|
|
18
18
|
return path.join(ME_BEHAVIOR, `${evidenceId}.json`);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* 현재 세션이 어느 host 에서 실행되는지 추론 (Multi-Host §4.2).
|
|
22
|
+
* 1) explicit `params.host`
|
|
23
|
+
* 2) env var `FORGEN_HOST` (e2e 격리용)
|
|
24
|
+
* 3) `runtime` env hint
|
|
25
|
+
* 4) Codex CLI 흔적 (`CODEX_HOME` 또는 `CODEX_SANDBOX_NETWORK_DISABLED`)
|
|
26
|
+
* 5) default 'claude' (1원칙)
|
|
27
|
+
*/
|
|
28
|
+
function detectHost(explicit) {
|
|
29
|
+
if (explicit)
|
|
30
|
+
return explicit;
|
|
31
|
+
const fromEnv = process.env.FORGEN_HOST;
|
|
32
|
+
if (fromEnv === 'claude' || fromEnv === 'codex')
|
|
33
|
+
return fromEnv;
|
|
34
|
+
if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX_NETWORK_DISABLED)
|
|
35
|
+
return 'codex';
|
|
36
|
+
return 'claude';
|
|
37
|
+
}
|
|
20
38
|
export function createEvidence(params) {
|
|
21
39
|
return {
|
|
22
40
|
evidence_id: crypto.randomUUID(),
|
|
@@ -29,9 +47,21 @@ export function createEvidence(params) {
|
|
|
29
47
|
candidate_rule_refs: params.candidate_rule_refs ?? [],
|
|
30
48
|
confidence: params.confidence,
|
|
31
49
|
raw_payload: params.raw_payload ?? {},
|
|
50
|
+
host: detectHost(params.host),
|
|
32
51
|
};
|
|
33
52
|
}
|
|
53
|
+
/** TEST-4 / RC4: behavior_observation 의 summary 가 의미있는 내용을 담아야 분석 가능. */
|
|
54
|
+
const MIN_BEHAVIOR_OBSERVATION_LEN = 20;
|
|
34
55
|
export function saveEvidence(evidence) {
|
|
56
|
+
// TEST-4 / RC4: 빈/짧은 behavior_observation 은 저장 거부.
|
|
57
|
+
// 결함: ~/.forgen/me/behavior/*.json 다수에 summary="" 가 누적되어 학습 데이터가
|
|
58
|
+
// 분석 불가능한 형태로 쌓임. saveEvidence 가 마지막 게이트라 여기서 거른다.
|
|
59
|
+
// 다른 evidence type (explicit_correction, session_summary) 은 backward compat.
|
|
60
|
+
if (evidence.type === 'behavior_observation') {
|
|
61
|
+
const len = (evidence.summary ?? '').trim().length;
|
|
62
|
+
if (len < MIN_BEHAVIOR_OBSERVATION_LEN)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
35
65
|
atomicWriteJSON(evidencePath(evidence.evidence_id), evidence, { pretty: true });
|
|
36
66
|
}
|
|
37
67
|
/**
|
|
@@ -73,8 +103,19 @@ export function appendEvidence(evidence) {
|
|
|
73
103
|
return { saved: true, t1_events: 0 };
|
|
74
104
|
}
|
|
75
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* 기존 evidence 에 host 필드가 없으면 'claude' 로 backfill (Multi-Host §4.2 마이그레이션 정책).
|
|
108
|
+
* 새 multi-host 도입 이전 데이터는 모두 Claude 에서 발생했음 — 이 backfill 은 무손실.
|
|
109
|
+
*/
|
|
110
|
+
function backfillHost(ev) {
|
|
111
|
+
if (!ev)
|
|
112
|
+
return ev;
|
|
113
|
+
if (ev.host === 'claude' || ev.host === 'codex')
|
|
114
|
+
return ev;
|
|
115
|
+
return { ...ev, host: 'claude' };
|
|
116
|
+
}
|
|
76
117
|
export function loadEvidence(evidenceId) {
|
|
77
|
-
return safeReadJSON(evidencePath(evidenceId), null);
|
|
118
|
+
return backfillHost(safeReadJSON(evidencePath(evidenceId), null));
|
|
78
119
|
}
|
|
79
120
|
export function loadAllEvidence() {
|
|
80
121
|
if (!fs.existsSync(ME_BEHAVIOR))
|
|
@@ -84,8 +125,9 @@ export function loadAllEvidence() {
|
|
|
84
125
|
if (!file.endsWith('.json'))
|
|
85
126
|
continue;
|
|
86
127
|
const ev = safeReadJSON(path.join(ME_BEHAVIOR, file), null);
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
const filled = backfillHost(ev);
|
|
129
|
+
if (filled)
|
|
130
|
+
items.push(filled);
|
|
89
131
|
}
|
|
90
132
|
return items;
|
|
91
133
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-mismatch demote signal — Multi-Host Core Design §4.3 / §10 우선순위 5
|
|
3
|
+
*
|
|
4
|
+
* spec §12.2 / §18 결과: schema-level 등가성이 강하므로 *호스트별 가중치* 가 아니라
|
|
5
|
+
* *불일치 신호* 만 사용한다. 같은 솔루션/규칙이 한 host 에서만 자주 깨지면 그 host 한정으로
|
|
6
|
+
* demote 후보. 본 모듈은 그 신호를 *읽기만* 한다 — 실제 demote 적용은 lifecycle 트랙.
|
|
7
|
+
*
|
|
8
|
+
* 신호 정의 (Phase 1 단순 버전):
|
|
9
|
+
* - solution_failed_to_apply_count_by_host
|
|
10
|
+
* - block_acknowledged_then_revert_count_by_host
|
|
11
|
+
* - drift_event_count_by_host
|
|
12
|
+
* 위 3 종 metric 을 evidence_id → host 매핑으로 집계.
|
|
13
|
+
*/
|
|
14
|
+
export type HostId = 'claude' | 'codex';
|
|
15
|
+
export interface HostMismatchSummary {
|
|
16
|
+
readonly byHost: Record<HostId, number>;
|
|
17
|
+
readonly total: number;
|
|
18
|
+
/** ratio of host with most events to total (0..1). 0.5 = balanced, 1.0 = single-host signal. */
|
|
19
|
+
readonly skew: number;
|
|
20
|
+
/** the dominant host (or null if balanced). */
|
|
21
|
+
readonly dominantHost: HostId | null;
|
|
22
|
+
/** 본 신호가 lifecycle demote 를 권고할 정도로 강한지. 임계값은 1차 단순. */
|
|
23
|
+
readonly demoteRecommended: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 특정 솔루션/규칙 ID 에 대해 *부정적 evidence* (drift, revert, failure) 가 host 별로
|
|
27
|
+
* 어떻게 분포하는지 요약.
|
|
28
|
+
*
|
|
29
|
+
* 1차 구현은 evidence.summary 의 ID 매칭으로 단순 집계. 향후 evidence 가 명시적 ref 필드를
|
|
30
|
+
* 가지면 그쪽으로 전환.
|
|
31
|
+
*/
|
|
32
|
+
export declare function summarizeNegativeSignalsForRef(refId: string): HostMismatchSummary;
|
|
33
|
+
export declare function summarizeAllByHost(): {
|
|
34
|
+
claude: number;
|
|
35
|
+
codex: number;
|
|
36
|
+
total: number;
|
|
37
|
+
};
|
|
38
|
+
/** 테스트 노출용 — 임계값이 변경되면 회귀 즉시 감지. */
|
|
39
|
+
export declare const HOST_MISMATCH_TUNING: {
|
|
40
|
+
readonly DOMINANCE_THRESHOLD: 0.8;
|
|
41
|
+
readonly MIN_TOTAL_FOR_DEMOTE: 5;
|
|
42
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-mismatch demote signal — Multi-Host Core Design §4.3 / §10 우선순위 5
|
|
3
|
+
*
|
|
4
|
+
* spec §12.2 / §18 결과: schema-level 등가성이 강하므로 *호스트별 가중치* 가 아니라
|
|
5
|
+
* *불일치 신호* 만 사용한다. 같은 솔루션/규칙이 한 host 에서만 자주 깨지면 그 host 한정으로
|
|
6
|
+
* demote 후보. 본 모듈은 그 신호를 *읽기만* 한다 — 실제 demote 적용은 lifecycle 트랙.
|
|
7
|
+
*
|
|
8
|
+
* 신호 정의 (Phase 1 단순 버전):
|
|
9
|
+
* - solution_failed_to_apply_count_by_host
|
|
10
|
+
* - block_acknowledged_then_revert_count_by_host
|
|
11
|
+
* - drift_event_count_by_host
|
|
12
|
+
* 위 3 종 metric 을 evidence_id → host 매핑으로 집계.
|
|
13
|
+
*/
|
|
14
|
+
import { loadAllEvidence } from './evidence-store.js';
|
|
15
|
+
const DOMINANCE_THRESHOLD = 0.8; // 80% 이상이 한 host 에서 발생하면 dominant.
|
|
16
|
+
const MIN_TOTAL_FOR_DEMOTE = 5; // 너무 적은 표본은 무시.
|
|
17
|
+
function summarize(events) {
|
|
18
|
+
const byHost = { claude: 0, codex: 0 };
|
|
19
|
+
for (const e of events) {
|
|
20
|
+
const h = (e.host ?? 'claude');
|
|
21
|
+
byHost[h] += 1;
|
|
22
|
+
}
|
|
23
|
+
const total = byHost.claude + byHost.codex;
|
|
24
|
+
if (total === 0) {
|
|
25
|
+
return { byHost, total, skew: 0, dominantHost: null, demoteRecommended: false };
|
|
26
|
+
}
|
|
27
|
+
const claudeShare = byHost.claude / total;
|
|
28
|
+
const codexShare = byHost.codex / total;
|
|
29
|
+
const dominant = claudeShare >= codexShare ? 'claude' : 'codex';
|
|
30
|
+
const skew = Math.max(claudeShare, codexShare);
|
|
31
|
+
const demoteRecommended = total >= MIN_TOTAL_FOR_DEMOTE && skew >= DOMINANCE_THRESHOLD;
|
|
32
|
+
return { byHost, total, skew, dominantHost: dominant, demoteRecommended };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 특정 솔루션/규칙 ID 에 대해 *부정적 evidence* (drift, revert, failure) 가 host 별로
|
|
36
|
+
* 어떻게 분포하는지 요약.
|
|
37
|
+
*
|
|
38
|
+
* 1차 구현은 evidence.summary 의 ID 매칭으로 단순 집계. 향후 evidence 가 명시적 ref 필드를
|
|
39
|
+
* 가지면 그쪽으로 전환.
|
|
40
|
+
*/
|
|
41
|
+
export function summarizeNegativeSignalsForRef(refId) {
|
|
42
|
+
const all = loadAllEvidence();
|
|
43
|
+
const matched = all.filter((e) => {
|
|
44
|
+
if (Array.isArray(e.candidate_rule_refs) && e.candidate_rule_refs.includes(refId))
|
|
45
|
+
return true;
|
|
46
|
+
if (typeof e.summary === 'string' && e.summary.includes(refId)) {
|
|
47
|
+
// 명시적 음수 키워드 (drift, revert, failed, block-revert) 가 있을 때만.
|
|
48
|
+
return /drift|revert|failed|regress/i.test(e.summary);
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
return summarize(matched);
|
|
53
|
+
}
|
|
54
|
+
export function summarizeAllByHost() {
|
|
55
|
+
const all = loadAllEvidence();
|
|
56
|
+
const r = { claude: 0, codex: 0 };
|
|
57
|
+
for (const e of all)
|
|
58
|
+
r[(e.host ?? 'claude')] += 1;
|
|
59
|
+
return { ...r, total: r.claude + r.codex };
|
|
60
|
+
}
|
|
61
|
+
/** 테스트 노출용 — 임계값이 변경되면 회귀 즉시 감지. */
|
|
62
|
+
export const HOST_MISMATCH_TUNING = {
|
|
63
|
+
DOMINANCE_THRESHOLD,
|
|
64
|
+
MIN_TOTAL_FOR_DEMOTE,
|
|
65
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — Implicit Feedback Store (TEST-5)
|
|
3
|
+
*
|
|
4
|
+
* `~/.forgen/state/implicit-feedback.jsonl` 의 append/read.
|
|
5
|
+
*
|
|
6
|
+
* TEST-5 / RC5: 누적된 엔트리들이 `type` 문자열만 가지고 category 없이 섞여 있어
|
|
7
|
+
* - drift_critical / drift_warning / revert_detected / repeated_edit / agent_* 가 한 스트림에 섞여
|
|
8
|
+
* - 집계/쿼리 시 카테고리 enum 부재로 휴리스틱 문자열 매칭에 의존
|
|
9
|
+
* - 스키마 검증이 없어 빈/잘못된 필드로 쓰여도 나중에 분석 불가
|
|
10
|
+
* 이 모듈은 category 필드를 **필수화**하고, 기존 레거시 라인은 read 시 `type→category`
|
|
11
|
+
* 백필로 보정한다. 새 write 는 category 없으면 drift/revert 계열은 **거부**한다.
|
|
12
|
+
*/
|
|
13
|
+
export declare const IMPLICIT_FEEDBACK_LOG: string;
|
|
14
|
+
/**
|
|
15
|
+
* TEST-5/H4: 카테고리 enum.
|
|
16
|
+
* - drift / revert: 네거티브 signal (schema 강제)
|
|
17
|
+
* - edit / agent: 네거티브-ish signal (휴리스틱)
|
|
18
|
+
* - positive: H4 양수 신호 — assist (recommendation_surfaced, recall_referenced)
|
|
19
|
+
*/
|
|
20
|
+
export type ImplicitFeedbackCategory = 'drift' | 'revert' | 'edit' | 'agent' | 'positive';
|
|
21
|
+
export interface ImplicitFeedbackEntry {
|
|
22
|
+
type: string;
|
|
23
|
+
category: ImplicitFeedbackCategory;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
at: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
/** 호출지 입력 — category 선택적 (schema 가 허용하면 inference 로 채움). */
|
|
29
|
+
export interface ImplicitFeedbackInput {
|
|
30
|
+
type: string;
|
|
31
|
+
category?: ImplicitFeedbackCategory;
|
|
32
|
+
sessionId?: string;
|
|
33
|
+
at: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
/** type → category 추론. 레거시 엔트리 마이그레이션과 호출지 기본값 계산에 공용. */
|
|
37
|
+
export declare function inferCategoryFromType(type: string): ImplicitFeedbackCategory | null;
|
|
38
|
+
/**
|
|
39
|
+
* TEST-5 메인 라이터. 내부에서 스키마 검증 후 append.
|
|
40
|
+
* drift/revert 스키마 위반 시 silent drop (hot path 에서 throw 금지).
|
|
41
|
+
* 반환값: 실제로 기록되었는지 (테스트 검증용).
|
|
42
|
+
*/
|
|
43
|
+
export declare function appendImplicitFeedback(entry: ImplicitFeedbackInput): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* TEST-5 리더. 세션 필터링 + 레거시 라인에 대한 lazy 마이그레이션 (category 백필).
|
|
46
|
+
* 디스크 상 파일은 건드리지 않고 읽기 시점에만 category 를 보정한다 — atomic-write
|
|
47
|
+
* 없이 append-only 로그를 rewrite 하면 race 위험이 있기 때문.
|
|
48
|
+
* 영구 백필은 `migrateImplicitFeedbackLog()` 를 명시적으로 호출한다.
|
|
49
|
+
*/
|
|
50
|
+
export declare function loadImplicitFeedback(sessionId: string): ImplicitFeedbackEntry[];
|
|
51
|
+
/**
|
|
52
|
+
* 영구 마이그레이션 — 레거시 로그 파일을 읽어 category 백필 후 원자적으로 재기록.
|
|
53
|
+
* 마이그레이션 불가 라인 (type 도 category 도 없거나 inference 실패) 은 drop.
|
|
54
|
+
* 반환: { migrated: 백필된 라인 수, dropped: 버려진 라인 수 }
|
|
55
|
+
*/
|
|
56
|
+
export declare function migrateImplicitFeedbackLog(): {
|
|
57
|
+
migrated: number;
|
|
58
|
+
dropped: number;
|
|
59
|
+
};
|