@wooojin/forgen 0.4.12 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +109 -0
- package/README.ja.md +4 -4
- package/README.md +89 -27
- package/README.zh.md +4 -4
- 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/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 +127 -79
- 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 +57 -0
- package/dist/core/compound-sweep-cli.js +354 -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 +161 -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 +11 -1
- package/dist/core/spawn.js +79 -7
- 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 +35 -6
- 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 +19 -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 +92 -46
- 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/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/exec-host.d.ts +4 -3
- package/dist/host/exec-host.js +2 -0
- 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 +8 -0
- package/dist/host/install-codex.js +2 -2
- 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 +20 -1
- package/dist/host/invoke-agent.d.ts +3 -2
- 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/projection.js +11 -0
- package/dist/mcp/tools.js +17 -4
- package/dist/store/evidence-store.d.ts +2 -6
- package/dist/store/evidence-store.js +68 -14
- 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/types.d.ts +9 -2
- package/package.json +3 -2
- package/plugin.json +2 -2
- package/skills/forge-loop/SKILL.md +7 -1
- package/skills/ship/SKILL.md +18 -1
|
@@ -16,8 +16,12 @@ import { promisify } from 'node:util';
|
|
|
16
16
|
const execFileAsync = promisify(execFile);
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
|
|
19
|
+
import { isSelfReferentialEcho } from './config-injector.js';
|
|
19
20
|
import { redactSecrets } from '../hooks/secret-filter.js';
|
|
21
|
+
import { stripPrivate } from '../engine/private-filter.js';
|
|
20
22
|
import { createEvidence, saveEvidence, promoteSessionCandidates } from '../store/evidence-store.js';
|
|
23
|
+
import { isHaikuCompoundEnabled } from './compound-consent.js';
|
|
24
|
+
import { summarizeTranscript } from './transcript-summary.js';
|
|
21
25
|
import { loadProfile } from '../store/profile-store.js';
|
|
22
26
|
import { FORGEN_HOME, ME_DIR } from './paths.js';
|
|
23
27
|
import { classifyBehaviorKind, mapKindToAxisRefs } from './behavior-classifier.js';
|
|
@@ -91,6 +95,23 @@ function execClaudeRetry(args, opts) {
|
|
|
91
95
|
* 본 함수는 0.4.7 작업의 foundation 으로 박제 — export 하여 unused warning 회피.
|
|
92
96
|
* 동작은 sync 버전과 동일: Claude/Codex 분기 + retry on transient.
|
|
93
97
|
*/
|
|
98
|
+
/**
|
|
99
|
+
* ADR-012 D: Haiku transcript-요약 추출은 **opt-in**. 동의 없으면 '' 반환 → 각 추출 phase 는
|
|
100
|
+
* fail-open(try/catch)이라 아무것도 쓰지 않고, 뒤의 `promoteSessionCandidates`(결정론·egress 0)
|
|
101
|
+
* 만 남는다. "우리는 당신 몰래 API 로 보내지 않는다".
|
|
102
|
+
*/
|
|
103
|
+
let haikuOptOutNoticeShown = false;
|
|
104
|
+
function extractViaHaiku(args, opts) {
|
|
105
|
+
if (!isHaikuCompoundEnabled()) {
|
|
106
|
+
if (!haikuOptOutNoticeShown) {
|
|
107
|
+
process.stderr.write('[forgen-auto-compound] Haiku 추출 opt-in 비활성 — 결정론 룰 승급만 수행 ' +
|
|
108
|
+
'(활성: `forgen compound consent on`)\n');
|
|
109
|
+
haikuOptOutNoticeShown = true;
|
|
110
|
+
}
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
return execClaudeRetry(args, opts);
|
|
114
|
+
}
|
|
94
115
|
export async function execClaudeRetryAsync(args, opts) {
|
|
95
116
|
const mod = createRequire(import.meta.url)('../host/exec-host.js');
|
|
96
117
|
const profileMod = createRequire(import.meta.url)('../store/profile-store.js');
|
|
@@ -216,79 +237,9 @@ function validateSolutionFiles(dirBefore) {
|
|
|
216
237
|
}
|
|
217
238
|
return removed;
|
|
218
239
|
}
|
|
219
|
-
|
|
220
|
-
if (typeof c === 'string')
|
|
221
|
-
return c;
|
|
222
|
-
if (Array.isArray(c)) {
|
|
223
|
-
return c
|
|
224
|
-
.filter((x) => typeof x === 'object' && x !== null && x.type === 'text')
|
|
225
|
-
.map((x) => (typeof x.text === 'string' ? x.text : ''))
|
|
226
|
-
.join('\n');
|
|
227
|
-
}
|
|
228
|
-
return '';
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* 0.4.6 — claude/codex JSONL 양 schema 호환.
|
|
232
|
-
*
|
|
233
|
-
* Claude: {type: 'user'|'assistant', content: ...}
|
|
234
|
-
* Codex: {type: 'response_item', payload: {type: 'message', role: 'user'|'assistant', content: [{type: 'input_text', text: ...}]}}
|
|
235
|
-
*/
|
|
240
|
+
/** 파일 → 요약. 순수 로직은 transcript-summary 모듈(테스트 가능). */
|
|
236
241
|
function extractSummary(filePath, maxChars = 8000) {
|
|
237
|
-
|
|
238
|
-
const lines = content.split('\n').filter(Boolean);
|
|
239
|
-
const messages = [];
|
|
240
|
-
let totalChars = 0;
|
|
241
|
-
for (const line of lines) {
|
|
242
|
-
try {
|
|
243
|
-
const entry = JSON.parse(line);
|
|
244
|
-
// Claude schema
|
|
245
|
-
if (entry.type === 'user' || entry.type === 'queue-operation') {
|
|
246
|
-
const text = extractText(entry.content);
|
|
247
|
-
if (text) {
|
|
248
|
-
messages.push(`[User] ${text.slice(0, 500)}`);
|
|
249
|
-
totalChars += text.length;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
else if (entry.type === 'assistant') {
|
|
253
|
-
const text = extractText(entry.content);
|
|
254
|
-
if (text) {
|
|
255
|
-
messages.push(`[Assistant] ${text.slice(0, 500)}`);
|
|
256
|
-
totalChars += text.length;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// Codex schema
|
|
260
|
-
else if (entry.type === 'response_item' && entry.payload?.role === 'user') {
|
|
261
|
-
const text = extractCodexText(entry.payload.content);
|
|
262
|
-
if (text) {
|
|
263
|
-
messages.push(`[User] ${text.slice(0, 500)}`);
|
|
264
|
-
totalChars += text.length;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
else if (entry.type === 'response_item' && entry.payload?.role === 'assistant') {
|
|
268
|
-
const text = extractCodexText(entry.payload.content);
|
|
269
|
-
if (text) {
|
|
270
|
-
messages.push(`[Assistant] ${text.slice(0, 500)}`);
|
|
271
|
-
totalChars += text.length;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
catch { /* skip */ }
|
|
276
|
-
if (totalChars > maxChars)
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
return messages.join('\n\n');
|
|
280
|
-
}
|
|
281
|
-
/** Codex content array → flat string. content: [{type: 'input_text', text: ...}] */
|
|
282
|
-
function extractCodexText(content) {
|
|
283
|
-
if (!Array.isArray(content))
|
|
284
|
-
return '';
|
|
285
|
-
const parts = [];
|
|
286
|
-
for (const item of content) {
|
|
287
|
-
if (item && typeof item === 'object' && 'text' in item && typeof item.text === 'string') {
|
|
288
|
-
parts.push(item.text);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return parts.join('\n').trim();
|
|
242
|
+
return summarizeTranscript(fs.readFileSync(filePath, 'utf-8'), maxChars);
|
|
292
243
|
}
|
|
293
244
|
/**
|
|
294
245
|
* 기존 behavior 파일에 유사 패턴이 있으면 observedCount를 +1 증가.
|
|
@@ -343,10 +294,21 @@ try {
|
|
|
343
294
|
// R5-G2 (P0 security): transcript 를 Claude 로 송신하기 전 API key / 토큰 / 비밀번호 /
|
|
344
295
|
// private key blocks 를 [REDACTED:...] 로 치환. 사용자가 채팅에 pasted 한 자격증명이
|
|
345
296
|
// auto-compound 를 통해 외부 API 로 누출되는 채널 차단.
|
|
346
|
-
const { redacted:
|
|
297
|
+
const { redacted: redactedSummary, hits: secretHits } = redactSecrets(rawSummary);
|
|
347
298
|
if (secretHits.length > 0) {
|
|
348
299
|
process.stderr.write(`[forgen-auto-compound] redacted ${secretHits.length} secret(s) before send: ${secretHits.map((s) => s.name).join(', ')}\n`);
|
|
349
300
|
}
|
|
301
|
+
// W2-5 (private 태그, flow-reviewer SEV-2): redactSecrets(보안) 와 직교하는 프라이버시
|
|
302
|
+
// 축. 이 auto-compound-runner 는 학습 코퍼스의 *주 자동 캡처 경로* — 세션 종료마다
|
|
303
|
+
// 전체 트랜스크립트를 추출 LLM 으로 보내고 behavior/solution 으로 파생한다. 사용자가
|
|
304
|
+
// <private> 로 표시한 범위를 LLM 송신·behavior write 전에 제거해야 "안심하고 교정"
|
|
305
|
+
// 약속이 주 경로에서 지켜진다.
|
|
306
|
+
const { cleaned: summary, hadPrivate } = stripPrivate(redactedSummary);
|
|
307
|
+
if (hadPrivate) {
|
|
308
|
+
process.stderr.write(`[forgen-auto-compound] excluded <private> range(s) before send (privacy)\n`);
|
|
309
|
+
}
|
|
310
|
+
if (summary.length < 200)
|
|
311
|
+
process.exit(0);
|
|
350
312
|
// 보안: 프롬프트 인젝션이 포함된 transcript는 분석하지 않음
|
|
351
313
|
if (containsPromptInjection(summary)) {
|
|
352
314
|
process.exit(0);
|
|
@@ -417,7 +379,7 @@ ${sanitizedSummary.slice(0, 6000)}
|
|
|
417
379
|
// 가 compound 추출용 forgen CLI 호출만 가능하게 한다. filter-bypass 시에도 임의
|
|
418
380
|
// 명령 실행 차단.
|
|
419
381
|
try {
|
|
420
|
-
|
|
382
|
+
extractViaHaiku(['-p', solutionPrompt, '--allowedTools', 'Bash(forgen compound:*)', '--model', COMPOUND_MODEL], { cwd, timeout: 90_000, stdio: ['pipe', 'ignore', 'pipe'] });
|
|
421
383
|
}
|
|
422
384
|
catch (e) {
|
|
423
385
|
process.stderr.write(`[forgen-auto-compound] solution extraction: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
@@ -449,7 +411,7 @@ ${sanitizedSummary.slice(0, 6000)}
|
|
|
449
411
|
${sanitizedSummary.slice(0, 4000)}
|
|
450
412
|
---`;
|
|
451
413
|
try {
|
|
452
|
-
const userResult =
|
|
414
|
+
const userResult = extractViaHaiku(['-p', userPrompt, '--model', COMPOUND_MODEL], {
|
|
453
415
|
cwd, timeout: 60_000, encoding: 'utf-8',
|
|
454
416
|
});
|
|
455
417
|
// 결과가 의미 있으면 behavior/ 파일로 저장
|
|
@@ -468,7 +430,14 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
468
430
|
if (isInjection) {
|
|
469
431
|
process.stderr.write(`[forgen-auto-compound] behavior: injection detected in LLM output, skipping write\n`);
|
|
470
432
|
}
|
|
471
|
-
|
|
433
|
+
// ADR-010 W1-2 캡처 사이드 에코 차단: Claude-voice 에코("이해했습니다…",
|
|
434
|
+
// "I see the notification…")를 저장 전에 거른다. 렌더 필터만으로는 스토어
|
|
435
|
+
// 오염이 누적된다 — 실측(2026-07-16) 오염 49건 전부 이 형태였음.
|
|
436
|
+
const isEcho = userResult ? isSelfReferentialEcho(userResult.trim()) : false;
|
|
437
|
+
if (isEcho && !isInjection) {
|
|
438
|
+
process.stderr.write(`[forgen-auto-compound] behavior: assistant-voice echo detected, skipping write\n`);
|
|
439
|
+
}
|
|
440
|
+
if (userResult && !isInjection && !isEcho && !userResult.includes('관찰된 패턴 없음') && userResult.trim().length > 10) {
|
|
472
441
|
userPatternFound = true; // 0.4.6 perf #12 — adaptive cooldown 시그널
|
|
473
442
|
fs.mkdirSync(BEHAVIOR_DIR, { recursive: true });
|
|
474
443
|
const today = new Date().toISOString().split('T')[0];
|
|
@@ -520,7 +489,9 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
520
489
|
${profileContext}
|
|
521
490
|
출력 형식 (JSON만, 설명 없이):
|
|
522
491
|
{
|
|
523
|
-
"corrections": [
|
|
492
|
+
"corrections": [
|
|
493
|
+
{ "policy": "이런 상황에서는 이렇게 하라(구체·행동가능)", "target": "무엇에 대한 교정인가(짧은 명사구)", "kind": "prefer-from-now" 또는 "avoid-this", "axis": "quality_safety" 또는 "autonomy" 또는 "judgment_philosophy" 또는 "communication_style" }
|
|
494
|
+
],
|
|
524
495
|
"observations": ["사용자의 반복 행동 패턴 목록"],
|
|
525
496
|
"pack_direction": null 또는 "opposite_quality" 또는 "opposite_autonomy",
|
|
526
497
|
"profile_delta": {
|
|
@@ -532,7 +503,15 @@ ${profileContext}
|
|
|
532
503
|
}
|
|
533
504
|
|
|
534
505
|
규칙:
|
|
535
|
-
- corrections:
|
|
506
|
+
- corrections: 사용자가 어시스턴트의 행동/접근/출력을 **교정한 지점**을 찾아라. 명시 마커
|
|
507
|
+
("하지마", "그렇게 말고", "앞으로는")뿐 아니라, **직전 [Assistant] 행동 대비 [User]가
|
|
508
|
+
방향을 튼 경우**도 포함:
|
|
509
|
+
· 긍정 지시형("~해줘", "~돌려봐" — 어시스턴트가 안 하던 걸 요구)
|
|
510
|
+
· 반사실형("~했어야 했다", "~에서 했어야 하는 거 아니야?")
|
|
511
|
+
· 완곡형("~안 해도 될 것 같아", "굳이 ~해야 할까?")
|
|
512
|
+
· 부정/반박형("아니", "그게 아니라", "틀렸어")
|
|
513
|
+
단순 진행 지시("다음 거 하자", "옵션 A로")나 질문은 제외. 없으면 빈 배열.
|
|
514
|
+
각 교정은 policy(재사용 가능한 규칙문, 20자 이상)·target·kind·axis 로 구조화.
|
|
536
515
|
- observations: 3회 이상 반복된 행동만. 없으면 빈 배열.
|
|
537
516
|
- pack_direction: 사용자가 현재 pack과 반대 방향으로 일관되게 행동했으면 opposite_quality 또는 opposite_autonomy. 아니면 null.
|
|
538
517
|
- profile_delta: facet 조정 제안. -0.1~+0.1 범위. 변화 없으면 0.0.
|
|
@@ -541,7 +520,7 @@ ${profileContext}
|
|
|
541
520
|
---
|
|
542
521
|
${sanitizedSummary.slice(0, 4000)}
|
|
543
522
|
---`;
|
|
544
|
-
const learningResult =
|
|
523
|
+
const learningResult = extractViaHaiku(['-p', learningSummaryPrompt, '--model', COMPOUND_MODEL], {
|
|
545
524
|
cwd, timeout: 60_000, encoding: 'utf-8',
|
|
546
525
|
});
|
|
547
526
|
// JSON 파싱 시도
|
|
@@ -570,6 +549,46 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
570
549
|
fs.mkdirSync(V1_EVIDENCE_DIR, { recursive: true });
|
|
571
550
|
fs.writeFileSync(path.join(V1_EVIDENCE_DIR, `${evidenceId}.json`), JSON.stringify(evidence, null, 2));
|
|
572
551
|
}
|
|
552
|
+
// ADR-013: 채굴된 교정을 **승급 가능 Evidence** 로 emit (기존엔 session_summary 로
|
|
553
|
+
// 죽던 데드엔드). auto_mined → promoteSessionCandidates 가 **advisory-only**(차단 불가) +
|
|
554
|
+
// "auto:" 네임스페이스 + 생성나이 은퇴로 승급. ①(실시간 명시 교정)과 엄격 차등.
|
|
555
|
+
// 안전(리뷰 SEV-1/2): (a) run 당 최대 MAX_MINED_PER_RUN 개만(무한 룰 주입 방지),
|
|
556
|
+
// (b) policy 를 injection 필터 통과(환각 payload 가 영구 룰이 되는 것 차단),
|
|
557
|
+
// (c) policy≥20 + target 필수.
|
|
558
|
+
const MAX_MINED_PER_RUN = 3;
|
|
559
|
+
let minedCorrections = 0;
|
|
560
|
+
for (const c of (parsed.corrections ?? [])) {
|
|
561
|
+
if (minedCorrections >= MAX_MINED_PER_RUN)
|
|
562
|
+
break; // 캡 (SEV-1 #3)
|
|
563
|
+
if (!c || typeof c !== 'object')
|
|
564
|
+
continue;
|
|
565
|
+
const policy = typeof c.policy === 'string' ? c.policy.trim() : '';
|
|
566
|
+
const target = typeof c.target === 'string' ? c.target.trim() : '';
|
|
567
|
+
const kind = c.kind === 'avoid-this' ? 'avoid-this' : 'prefer-from-now';
|
|
568
|
+
const axis = typeof c.axis === 'string' ? c.axis : null;
|
|
569
|
+
if (policy.length < 20 || !target)
|
|
570
|
+
continue;
|
|
571
|
+
// 출력 sanitize (SEV-2 #5): 채굴 policy 는 미래 세션 프롬프트에 룰로 주입되므로,
|
|
572
|
+
// injection payload 를 룰화하지 않도록 입력과 동일한 필터를 출력에도 적용.
|
|
573
|
+
if (containsPromptInjection(policy) || containsPromptInjection(target)) {
|
|
574
|
+
process.stderr.write('[forgen-auto-compound] skipped mined correction (injection filter)\n');
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const ev = createEvidence({
|
|
578
|
+
type: 'explicit_correction',
|
|
579
|
+
session_id: sessionId,
|
|
580
|
+
source_component: 'auto-compound-miner',
|
|
581
|
+
summary: policy,
|
|
582
|
+
axis_refs: axis ? [axis] : [],
|
|
583
|
+
confidence: 0.55, // ①(실시간)보다 낮게 — 채굴 불확실성 반영
|
|
584
|
+
raw_payload: { kind, target, axis_hint: axis, auto_mined: true },
|
|
585
|
+
});
|
|
586
|
+
saveEvidence(ev);
|
|
587
|
+
minedCorrections++;
|
|
588
|
+
}
|
|
589
|
+
if (minedCorrections > 0) {
|
|
590
|
+
process.stderr.write(`[forgen-auto-compound] mined ${minedCorrections} correction(s) (advisory-only, capped, age-retired)\n`);
|
|
591
|
+
}
|
|
573
592
|
// facet delta 적용
|
|
574
593
|
if (parsed.profile_delta) {
|
|
575
594
|
const profile = JSON.parse(fs.readFileSync(V1_PROFILE, 'utf-8'));
|
|
@@ -638,6 +657,20 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
638
657
|
catch (e) {
|
|
639
658
|
process.stderr.write(`[forgen-auto-compound] rule promotion: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
640
659
|
}
|
|
660
|
+
// Step 4b (W3-2): 교정 클러스터링 — 같은 원칙을 N회 교정해 흩어진 룰을 1개 명명 룰로
|
|
661
|
+
// 통합하고 강도를 반복 신호(Laplace)로 승급. 승급 직후 같은 패스에서 실행. fail-open.
|
|
662
|
+
try {
|
|
663
|
+
const { runCorrectionClustering } = await import('../engine/correction-cluster-runner.js');
|
|
664
|
+
const merges = await runCorrectionClustering();
|
|
665
|
+
for (const m of merges) {
|
|
666
|
+
process.stderr.write(`[forgen-auto-compound] clustered ${m.memberIds.length} correction(s) → 1 rule ` +
|
|
667
|
+
`[${m.category}, strength=${m.strength}, conf=${m.confidence.toFixed(2)}] — ` +
|
|
668
|
+
`undo: forgen rule unmerge-cluster ${m.mergedRuleId}\n`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch (e) {
|
|
672
|
+
process.stderr.write(`[forgen-auto-compound] correction clustering: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
673
|
+
}
|
|
641
674
|
// H2: count newly extracted solutions (post-quality-gate) for Stop hook 알림.
|
|
642
675
|
// solutionsBefore 스냅샷 vs 현재 디스크 상태 차분 → "N개 패턴 학습됨" 1줄.
|
|
643
676
|
let extractedSolutionsCount = 0;
|
|
@@ -682,6 +715,18 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
682
715
|
catch (e) {
|
|
683
716
|
process.stderr.write(`[forgen-gc] state prune failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
684
717
|
}
|
|
718
|
+
// ADR-010 W3-1 (F2): 세션 종료마다 ROI 재판정 — surfaced≫acted_on 솔루션
|
|
719
|
+
// 강등/격리. δ 가 100% injection 에서 나오므로(실측) 주입 품질 = 효과.
|
|
720
|
+
try {
|
|
721
|
+
const { updateRoiDemotions } = await import('../engine/roi-demotion.js');
|
|
722
|
+
const roi = updateRoiDemotions();
|
|
723
|
+
if (roi && (roi.demoted > 0 || roi.quarantined > 0)) {
|
|
724
|
+
process.stderr.write(`[forgen-roi] low-ROI solutions: ${roi.demoted} demoted, ${roi.quarantined} quarantined\n`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
process.stderr.write(`[forgen-roi] update failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
729
|
+
}
|
|
685
730
|
// Step 6 (v0.4.1): rule lifecycle 자동 실행 — rule 의 violations/bypass/drift
|
|
686
731
|
// 신호에 따른 자동 강등/승격. 이전에는 CLI (`forgen rule scan --apply`) 수동
|
|
687
732
|
// 호출만 있어서 구매자가 몇 주 써도 rule 정비 안 됨 → 쓸모없는 rule 이 계속
|
|
@@ -721,6 +766,9 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
721
766
|
extractedSolutions: extractedSolutionsCount,
|
|
722
767
|
promotedRules: promotedCount,
|
|
723
768
|
userPatternFound, // 0.4.6 perf #12 — adaptive cooldown 시그널
|
|
769
|
+
// ADR-011 B수정: 이 run 시점의 프롬프트 수 기록 → 이후 barren backoff 결정 시
|
|
770
|
+
// 세션이 자라났으면(promptCount 증가) barren 을 무시하고 재추출하기 위함.
|
|
771
|
+
promptCount: Number(process.argv[5] ?? 0) || 0,
|
|
724
772
|
noticeShown: false,
|
|
725
773
|
}));
|
|
726
774
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ConsentSource = 'env-off' | 'env-on' | 'config' | 'default';
|
|
2
|
+
export interface ConsentState {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
source: ConsentSource;
|
|
5
|
+
}
|
|
6
|
+
/** 순수: env + config 값으로 Haiku 동의 해석 (테스트 가능). */
|
|
7
|
+
export declare function resolveHaikuConsent(env: {
|
|
8
|
+
no?: string;
|
|
9
|
+
on?: string;
|
|
10
|
+
}, configValue: boolean | undefined): ConsentState;
|
|
11
|
+
export declare function getHaikuConsentState(): ConsentState;
|
|
12
|
+
/** 러너가 Haiku 전송 전에 확인. */
|
|
13
|
+
export declare function isHaikuCompoundEnabled(): boolean;
|
|
14
|
+
/** 동의 저장(config.json 병합). env override 는 건드리지 않음. */
|
|
15
|
+
export declare function setHaikuCompoundConsent(enabled: boolean): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto-compound Haiku 추출 동의 (ADR-012 D 티어드).
|
|
3
|
+
*
|
|
4
|
+
* 결정론 경로(교정→룰 승급·notepad 주입, egress 0)는 항상 동작하고, **Haiku 로 transcript
|
|
5
|
+
* 요약을 전송하는 추출만** 이 동의로 게이트한다(default opt-in=off). "우리는 당신 몰래
|
|
6
|
+
* API 로 보내지 않는다".
|
|
7
|
+
*
|
|
8
|
+
* 우선순위: FORGEN_NO_AUTO_COMPOUND(강제 off) > FORGEN_AUTO_COMPOUND(강제 on) >
|
|
9
|
+
* config(~/.forgen/config.json.autoCompoundHaiku) > default(false).
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { FORGEN_HOME } from './paths.js';
|
|
14
|
+
/** 순수: env + config 값으로 Haiku 동의 해석 (테스트 가능). */
|
|
15
|
+
export function resolveHaikuConsent(env, configValue) {
|
|
16
|
+
if (env.no === '1')
|
|
17
|
+
return { enabled: false, source: 'env-off' };
|
|
18
|
+
if (env.on === '1')
|
|
19
|
+
return { enabled: true, source: 'env-on' };
|
|
20
|
+
if (typeof configValue === 'boolean')
|
|
21
|
+
return { enabled: configValue, source: 'config' };
|
|
22
|
+
return { enabled: false, source: 'default' }; // ADR-012: opt-in
|
|
23
|
+
}
|
|
24
|
+
const CONFIG_PATH = path.join(FORGEN_HOME, 'config.json');
|
|
25
|
+
function readConfig() {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function getHaikuConsentState() {
|
|
34
|
+
const cfg = readConfig();
|
|
35
|
+
const configValue = typeof cfg.autoCompoundHaiku === 'boolean' ? cfg.autoCompoundHaiku : undefined;
|
|
36
|
+
return resolveHaikuConsent({ no: process.env.FORGEN_NO_AUTO_COMPOUND, on: process.env.FORGEN_AUTO_COMPOUND }, configValue);
|
|
37
|
+
}
|
|
38
|
+
/** 러너가 Haiku 전송 전에 확인. */
|
|
39
|
+
export function isHaikuCompoundEnabled() {
|
|
40
|
+
return getHaikuConsentState().enabled;
|
|
41
|
+
}
|
|
42
|
+
/** 동의 저장(config.json 병합). env override 는 건드리지 않음. */
|
|
43
|
+
export function setHaikuCompoundConsent(enabled) {
|
|
44
|
+
const cfg = readConfig();
|
|
45
|
+
cfg.autoCompoundHaiku = enabled;
|
|
46
|
+
fs.mkdirSync(FORGEN_HOME, { recursive: true });
|
|
47
|
+
fs.writeFileSync(CONFIG_PATH, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** sweep 대상 후보의 최소 메타. */
|
|
2
|
+
export interface TranscriptMeta {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
transcriptPath: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
promptCount: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SweepOpts {
|
|
10
|
+
nowMs: number;
|
|
11
|
+
/** 이 시간 내 수정된 transcript 만 대상 (기본 24h). */
|
|
12
|
+
windowMs: number;
|
|
13
|
+
/** 세션이 이만큼 지나야 재-sweep (기본 2h). */
|
|
14
|
+
staleMs: number;
|
|
15
|
+
/** 최소 user 프롬프트 수 (기본 10). */
|
|
16
|
+
minPrompts: number;
|
|
17
|
+
/** 한 sweep 당 최대 세션 수 (비용 상한, 기본 5). */
|
|
18
|
+
maxPerRun: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const DEFAULT_SWEEP_OPTS: Omit<SweepOpts, 'nowMs'>;
|
|
21
|
+
type SweepState = Record<string, {
|
|
22
|
+
sweptAt: number;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* 순수 선택: 최근·충분한 대화·stale(오래 안 쓸린) 세션을, oldest-swept 우선으로 maxPerRun 만큼.
|
|
26
|
+
*/
|
|
27
|
+
export declare function selectSweepCandidates(transcripts: TranscriptMeta[], sweepState: SweepState, opts: SweepOpts): TranscriptMeta[];
|
|
28
|
+
/** Claude projects 하위의 최근 transcript 를 열거해 메타 구성. */
|
|
29
|
+
export declare function enumerateTranscripts(windowMs: number, nowMs: number): TranscriptMeta[];
|
|
30
|
+
export interface SweepResult {
|
|
31
|
+
scanned: number;
|
|
32
|
+
eligible: number;
|
|
33
|
+
swept: string[];
|
|
34
|
+
stateSaveFailed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* cron/CLI 진입점. dryRun 이면 실행 없이 후보만 보고.
|
|
38
|
+
* 무인 실행 안전장치: (1) 동시 실행 lock, (2) 실제 spawn('spawned') 만 sweptAt 기록,
|
|
39
|
+
* (3) state 저장 실패 시 경고 플래그.
|
|
40
|
+
*/
|
|
41
|
+
export declare function runCompoundSweep(opts?: {
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
} & Partial<Omit<SweepOpts, 'nowMs'>>): SweepResult;
|
|
44
|
+
/** crontab 라인 구성 (순수 — 테스트 가능). 경로는 shell-quote(리뷰 SEV-2). */
|
|
45
|
+
export declare function buildCronLine(nodePath: string, cliPath: string, logPath: string, schedule?: string): string;
|
|
46
|
+
/** 기존 crontab 에서 forgen 라인을 upsert/제거 (순수 — 테스트 가능). */
|
|
47
|
+
export declare function upsertCronText(existing: string, line: string | null): string;
|
|
48
|
+
export interface CronOpResult {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function installCron(): CronOpResult;
|
|
53
|
+
export declare function uninstallCron(): CronOpResult;
|
|
54
|
+
export declare function cronStatus(): CronOpResult;
|
|
55
|
+
/** CLI: forgen compound sweep [--dry-run|--install-cron|--uninstall-cron|--cron-status] [--window-hours N] [--stale-hours M] */
|
|
56
|
+
export declare function compoundSweepCli(args: string[]): void;
|
|
57
|
+
export {};
|