@wooojin/forgen 0.4.13 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +261 -0
- package/README.ja.md +16 -12
- package/README.ko.md +12 -8
- package/README.md +90 -27
- package/README.zh.md +16 -12
- package/assets/claude/commands/forge-loop.md +7 -1
- package/assets/claude/commands/ship.md +18 -1
- package/assets/opencode/forgen.ts +52 -0
- package/assets/shared/hook-registry.json +210 -22
- package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
- package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
- package/dist/checks/_shared/model-profile.d.ts +25 -0
- package/dist/checks/_shared/model-profile.js +63 -0
- package/dist/cli.js +99 -153
- package/dist/core/auto-compound-runner.d.ts +0 -11
- package/dist/core/auto-compound-runner.js +199 -92
- package/dist/core/changelog-cli.d.ts +4 -1
- package/dist/core/changelog-cli.js +8 -7
- package/dist/core/compound-consent.d.ts +15 -0
- package/dist/core/compound-consent.js +48 -0
- package/dist/core/compound-sweep-cli.d.ts +60 -0
- package/dist/core/compound-sweep-cli.js +374 -0
- package/dist/core/config-injector.d.ts +16 -1
- package/dist/core/config-injector.js +47 -28
- package/dist/core/dashboard-cli.js +40 -16
- package/dist/core/dashboard.d.ts +3 -4
- package/dist/core/dashboard.js +15 -34
- package/dist/core/dev-cli.d.ts +13 -0
- package/dist/core/dev-cli.js +70 -0
- package/dist/core/doctor.d.ts +16 -5
- package/dist/core/doctor.js +190 -176
- package/dist/core/drift-score.d.ts +2 -0
- package/dist/core/drift-score.js +9 -1
- package/dist/core/harness.js +115 -43
- package/dist/core/health-cli.d.ts +2 -0
- package/dist/core/health-cli.js +6 -1
- package/dist/core/host-detect.d.ts +3 -1
- package/dist/core/host-detect.js +26 -1
- package/dist/core/migrate-cli.js +15 -0
- package/dist/core/migrate-evidence-host.d.ts +2 -1
- package/dist/core/migrate-tenetx.d.ts +50 -0
- package/dist/core/migrate-tenetx.js +262 -0
- package/dist/core/probe-workflow-cli.d.ts +3 -3
- package/dist/core/probe-workflow-cli.js +13 -13
- package/dist/core/recall-cli.js +1 -1
- package/dist/core/regress-map-cli.js +1 -1
- package/dist/core/rendered-rules-manifest.d.ts +29 -0
- package/dist/core/rendered-rules-manifest.js +60 -0
- package/dist/core/session-store.js +14 -3
- package/dist/core/settings-injector.d.ts +3 -0
- package/dist/core/settings-injector.js +12 -16
- package/dist/core/spawn.d.ts +37 -1
- package/dist/core/spawn.js +116 -9
- package/dist/core/state-gc.js +1 -0
- package/dist/core/status-cli.d.ts +20 -0
- package/dist/core/status-cli.js +100 -0
- package/dist/core/statusline-cli.d.ts +7 -0
- package/dist/core/statusline-cli.js +63 -19
- package/dist/core/transcript-summary.d.ts +18 -0
- package/dist/core/transcript-summary.js +81 -0
- package/dist/core/trust-layer-intent.d.ts +21 -1
- package/dist/core/trust-layer-intent.js +7 -0
- package/dist/core/types.d.ts +3 -2
- package/dist/core/uninstall.js +12 -0
- package/dist/core/usage-telemetry.d.ts +7 -1
- package/dist/core/usage-telemetry.js +7 -9
- package/dist/core/v1-bootstrap.js +1 -1
- package/dist/core/watch-cli.js +1 -1
- package/dist/engine/compound-extractor.js +10 -0
- package/dist/engine/compound-loop.js +55 -7
- package/dist/engine/compound-share.d.ts +85 -0
- package/dist/engine/compound-share.js +606 -0
- package/dist/engine/correction-cluster-runner.d.ts +38 -0
- package/dist/engine/correction-cluster-runner.js +188 -0
- package/dist/engine/correction-clustering.d.ts +80 -0
- package/dist/engine/correction-clustering.js +167 -0
- package/dist/engine/enforce-classifier.d.ts +10 -1
- package/dist/engine/enforce-classifier.js +111 -22
- package/dist/engine/extraction-session.js +10 -3
- package/dist/engine/private-filter.d.ts +36 -0
- package/dist/engine/private-filter.js +100 -0
- package/dist/engine/ranking-pipeline.js +4 -2
- package/dist/engine/relevance-gate.d.ts +12 -0
- package/dist/engine/relevance-gate.js +12 -0
- package/dist/engine/roi-demotion.d.ts +79 -0
- package/dist/engine/roi-demotion.js +159 -0
- package/dist/engine/solution-format.d.ts +1 -0
- package/dist/engine/solution-format.js +26 -0
- package/dist/engine/solution-matcher.js +10 -1
- package/dist/fgx.js +7 -6
- package/dist/forge/cli.js +8 -2
- package/dist/hooks/compound-reflection.js +6 -1
- package/dist/hooks/context-guard.d.ts +16 -1
- package/dist/hooks/context-guard.js +99 -49
- package/dist/hooks/hook-config.js +5 -0
- package/dist/hooks/hook-registry.d.ts +6 -1
- package/dist/hooks/hooks-generator.js +3 -0
- package/dist/hooks/post-tool-use.js +2 -3
- package/dist/hooks/pre-compact.js +14 -0
- package/dist/hooks/pre-tool-use.js +5 -1
- package/dist/hooks/session-end.d.ts +34 -0
- package/dist/hooks/session-end.js +100 -0
- package/dist/hooks/session-recovery.js +18 -1
- package/dist/hooks/shared/hook-timing.js +6 -1
- package/dist/hooks/shared/stop-triggers.d.ts +29 -2
- package/dist/hooks/shared/stop-triggers.js +35 -2
- package/dist/hooks/solution-injector.d.ts +28 -0
- package/dist/hooks/solution-injector.js +126 -28
- package/dist/hooks/stop-guard.js +5 -2
- package/dist/hooks/subagent-stop-guard.js +4 -1
- package/dist/host/capabilities-claude.js +1 -0
- package/dist/host/capabilities-codex.js +1 -0
- package/dist/host/capabilities-opencode.d.ts +26 -0
- package/dist/host/capabilities-opencode.js +78 -0
- package/dist/host/capabilities-registry.d.ts +7 -0
- package/dist/host/capabilities-registry.js +14 -0
- package/dist/host/codex-rules-context.d.ts +25 -0
- package/dist/host/codex-rules-context.js +62 -0
- package/dist/host/exec-host.d.ts +14 -3
- package/dist/host/exec-host.js +15 -2
- package/dist/host/host-binding.d.ts +27 -0
- package/dist/host/host-binding.js +11 -0
- package/dist/host/host-runtime.js +18 -0
- package/dist/host/install-codex.d.ts +66 -0
- package/dist/host/install-codex.js +293 -25
- package/dist/host/install-opencode.d.ts +38 -0
- package/dist/host/install-opencode.js +148 -0
- package/dist/host/install-orchestrator.d.ts +4 -1
- package/dist/host/install-orchestrator.js +30 -1
- package/dist/host/invoke-agent.d.ts +3 -2
- package/dist/host/invoke-agent.js +1 -0
- package/dist/host/opencode/context-cli.d.ts +15 -0
- package/dist/host/opencode/context-cli.js +25 -0
- package/dist/host/opencode/guard-cli.d.ts +13 -0
- package/dist/host/opencode/guard-cli.js +39 -0
- package/dist/host/opencode/plugin/forgen.d.ts +31 -0
- package/dist/host/opencode/plugin/forgen.js +60 -0
- package/dist/host/opencode/translate.d.ts +49 -0
- package/dist/host/opencode/translate.js +96 -0
- package/dist/host/parity-harness.d.ts +10 -2
- package/dist/host/parity-harness.js +7 -3
- package/dist/host/projection.d.ts +37 -3
- package/dist/host/projection.js +104 -42
- package/dist/mcp/tools.js +17 -4
- package/dist/store/evidence-store.d.ts +2 -6
- package/dist/store/evidence-store.js +164 -46
- package/dist/store/host-mismatch.d.ts +2 -1
- package/dist/store/host-mismatch.js +8 -8
- package/dist/store/profile-store.d.ts +3 -2
- package/dist/store/rule-store.d.ts +41 -0
- package/dist/store/rule-store.js +77 -2
- package/dist/store/types.d.ts +9 -2
- package/hooks/hooks.json +13 -1
- package/package.json +3 -2
- package/plugin.json +2 -2
- package/skills/architecture-decision/SKILL.md +18 -0
- package/skills/calibrate/SKILL.md +18 -0
- package/skills/code-review/SKILL.md +17 -0
- package/skills/compound/SKILL.md +17 -0
- package/skills/deep-interview/SKILL.md +7 -0
- package/skills/docker/SKILL.md +18 -0
- package/skills/forge-loop/SKILL.md +30 -1
- package/skills/learn/SKILL.md +15 -0
- package/skills/retro/SKILL.md +16 -0
- package/skills/ship/SKILL.md +36 -1
|
@@ -80,9 +80,11 @@ export const SCENARIO_CORPUS = [
|
|
|
80
80
|
claude: { decision: 'block', reason: 'tests not yet executed' },
|
|
81
81
|
codex: { decision: 'block', reason: 'tests not yet executed' },
|
|
82
82
|
},
|
|
83
|
+
// ADR-015 G1: Stop block 은 top-level decision/reason 으로 전달된다 (양 host 동일 스키마).
|
|
83
84
|
compareKeys: [
|
|
84
85
|
'continue',
|
|
85
|
-
'
|
|
86
|
+
'decision',
|
|
87
|
+
'reason',
|
|
86
88
|
],
|
|
87
89
|
},
|
|
88
90
|
{
|
|
@@ -106,9 +108,11 @@ export const SCENARIO_CORPUS = [
|
|
|
106
108
|
},
|
|
107
109
|
},
|
|
108
110
|
},
|
|
111
|
+
// 0.5.4: Codex PostToolUse 스키마는 hookSpecificOutput.permissionDecision 을 모른다 —
|
|
112
|
+
// 사영이 top-level decision:"block" + reason 으로 번역하므로 그 키로 동치 비교.
|
|
109
113
|
compareKeys: [
|
|
110
|
-
'
|
|
111
|
-
'
|
|
114
|
+
'decision',
|
|
115
|
+
'reason',
|
|
112
116
|
],
|
|
113
117
|
},
|
|
114
118
|
{
|
|
@@ -16,12 +16,45 @@
|
|
|
16
16
|
import type { HookEventInput, HookEventOutput } from '../core/types.js';
|
|
17
17
|
import type { HostId } from '../core/trust-layer-intent.js';
|
|
18
18
|
export type ProjectToClaudeEvent = (raw: unknown, input: HookEventInput) => HookEventOutput;
|
|
19
|
+
interface DecisionView {
|
|
20
|
+
continueFlag: boolean;
|
|
21
|
+
permissionDecision?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function parseDecision(raw: unknown): DecisionView;
|
|
19
24
|
/**
|
|
20
|
-
* Codex 출력
|
|
25
|
+
* Codex 출력 정규화 — **Codex 0.153.4 hook 출력 스키마 준수** (ADR-015 G1 + 0.5.4 수정).
|
|
21
26
|
*
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
27
|
+
* 이 함수의 출력은 *Codex 가 읽는다* (codex-adapter 가 stdout 으로 내보냄). Codex 는 이벤트별 출력
|
|
28
|
+
* 스키마가 `additionalProperties:false` 라서 **허용되지 않은 키가 하나라도 있으면 출력 전체를 버리고
|
|
29
|
+
* "hook returned invalid ... JSON output" → Failed** 로 처리한다 (codex-rs/hooks/src/engine/
|
|
30
|
+
* output_parser.rs `parse_json` + events/stop.rs `parse_completed`; 스키마 사본은
|
|
31
|
+
* tests/fixtures/codex-hook-schemas/). 특히 Stop/SubagentStop 은 `hookSpecificOutput` 자체를
|
|
32
|
+
* 허용하지 않는다 — 0.5.3 의 사영이 모든 출력에 `hookSpecificOutput.hookEventName` 을 붙여 실환경에서
|
|
33
|
+
* Stop 훅 2개가 매 턴 Failed 로 떨어졌다 (0.5.4 에서 수정, 실세션 재현 후).
|
|
34
|
+
*
|
|
35
|
+
* 규칙 (이벤트별 allowlist — CODEX_OUTPUT_SCHEMA):
|
|
36
|
+
* 1. 객체가 아니면 fail-open `{ continue: true }`.
|
|
37
|
+
* 2. universal 키(`continue`/`stopReason`/`suppressOutput`/`systemMessage`) 보존 (Interrupt 는
|
|
38
|
+
* systemMessage 만).
|
|
39
|
+
* 3. `decision`/`reason` 은 PreToolUse/PostToolUse/UserPromptSubmit/Stop/SubagentStop 에서만 보존.
|
|
40
|
+
* `decision:"block"` 인데 reason 이 비면 systemMessage → 고정 문구로 보강 (Codex 가 거부).
|
|
41
|
+
* 4. `hookSpecificOutput` 은 허용 이벤트에서만, 허용 하위 키만 남기고 `hookEventName` 을 이벤트명으로
|
|
42
|
+
* 고정한다. Stop/SubagentStop/PreCompact/PostCompact/Interrupt 에서는 통째로 제거. 절대 새로
|
|
43
|
+
* 만들어 붙이지 않는다.
|
|
44
|
+
* 5. PostToolUse 에 forgen 이 `permissionDecision:"deny"` (PreToolUse 형) 를 냈으면 Codex 의
|
|
45
|
+
* PostToolUse block 형(top-level `decision:"block"` + `reason`) 으로 번역.
|
|
46
|
+
* 6. PreToolUse: `permissionDecision` 이 있으면 `continue:false` 제거 (Codex "unsupported").
|
|
47
|
+
* 7. legacy `approved:false` → `hookSpecificOutput.permissionDecision:"deny"` (PreToolUse 한정).
|
|
48
|
+
* 8. 이벤트명을 모르면(입력에 없음) 키를 깎지 않고 pass-through 한다 — 모르면 건드리지 않는다.
|
|
24
49
|
*/
|
|
50
|
+
interface EventOutputPolicy {
|
|
51
|
+
universal: ReadonlyArray<string>;
|
|
52
|
+
decision: boolean;
|
|
53
|
+
/** hookSpecificOutput 허용 하위 키. undefined = hookSpecificOutput 자체 불허. */
|
|
54
|
+
hso?: ReadonlyArray<string>;
|
|
55
|
+
}
|
|
56
|
+
/** codex-rs/hooks/schema/generated/*.command.output.schema.json (rust-v0.153.4) 요약. */
|
|
57
|
+
export declare const CODEX_OUTPUT_SCHEMA: Readonly<Record<string, EventOutputPolicy>>;
|
|
25
58
|
export declare const projectCodexToClaude: ProjectToClaudeEvent;
|
|
26
59
|
/**
|
|
27
60
|
* Claude 어댑터의 사영. 1원칙(Claude reference) + spec §18.4 (Codex hooks.json schema 동일성)
|
|
@@ -33,3 +66,4 @@ export declare const projectCodexToClaude: ProjectToClaudeEvent;
|
|
|
33
66
|
*/
|
|
34
67
|
export declare const projectClaudeToClaude: ProjectToClaudeEvent;
|
|
35
68
|
export declare function getProjection(host: HostId): ProjectToClaudeEvent;
|
|
69
|
+
export {};
|
package/dist/host/projection.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* semantics 알아도 됨, Codex 표면 모름). 즉 Codex CLI 의 stdout 을 받아 코어가 학습 가능한
|
|
14
14
|
* Claude 형 객체로 변환만 한다.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
// parseDecision 은 구 사영(결정 → continue:false 변환) 의 잔재. ADR-015 G1 이후 Codex 사영은
|
|
17
|
+
// 호스트 스키마를 보존하므로 사용하지 않는다. 다른 host binding 이 참고할 수 있어 export 만 유지.
|
|
18
|
+
export function parseDecision(raw) {
|
|
17
19
|
if (typeof raw === 'boolean')
|
|
18
20
|
return { continueFlag: raw };
|
|
19
21
|
if (typeof raw === 'string') {
|
|
@@ -51,58 +53,107 @@ function parseDecision(raw) {
|
|
|
51
53
|
}
|
|
52
54
|
return { continueFlag: true };
|
|
53
55
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const UNIVERSAL = ['continue', 'stopReason', 'suppressOutput', 'systemMessage'];
|
|
57
|
+
/** codex-rs/hooks/schema/generated/*.command.output.schema.json (rust-v0.153.4) 요약. */
|
|
58
|
+
export const CODEX_OUTPUT_SCHEMA = {
|
|
59
|
+
SessionStart: { universal: UNIVERSAL, decision: false, hso: ['hookEventName', 'additionalContext'] },
|
|
60
|
+
SubagentStart: { universal: UNIVERSAL, decision: false, hso: ['hookEventName', 'additionalContext'] },
|
|
61
|
+
UserPromptSubmit: { universal: UNIVERSAL, decision: true, hso: ['hookEventName', 'additionalContext'] },
|
|
62
|
+
PreToolUse: {
|
|
63
|
+
universal: UNIVERSAL,
|
|
64
|
+
decision: true,
|
|
65
|
+
hso: ['hookEventName', 'additionalContext', 'permissionDecision', 'permissionDecisionReason', 'updatedInput'],
|
|
66
|
+
},
|
|
67
|
+
PostToolUse: { universal: UNIVERSAL, decision: true, hso: ['hookEventName', 'additionalContext', 'updatedMCPToolOutput'] },
|
|
68
|
+
PermissionRequest: { universal: UNIVERSAL, decision: false, hso: ['hookEventName', 'decision'] },
|
|
69
|
+
Stop: { universal: UNIVERSAL, decision: true },
|
|
70
|
+
SubagentStop: { universal: UNIVERSAL, decision: true },
|
|
71
|
+
PreCompact: { universal: UNIVERSAL, decision: false },
|
|
72
|
+
PostCompact: { universal: UNIVERSAL, decision: false },
|
|
73
|
+
Interrupt: { universal: ['systemMessage'], decision: false },
|
|
74
|
+
};
|
|
75
|
+
function resolveEventName(raw, input) {
|
|
76
|
+
const hso = raw.hookSpecificOutput;
|
|
77
|
+
const fromOutput = hso && typeof hso === 'object' ? hso.hookEventName : undefined;
|
|
78
|
+
const candidate = input.hookEventName
|
|
79
|
+
?? input.hook_event_name // 실 stdin 은 snake_case
|
|
80
|
+
?? input.event
|
|
81
|
+
?? (typeof fromOutput === 'string' ? fromOutput : undefined);
|
|
82
|
+
return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;
|
|
83
|
+
}
|
|
60
84
|
export const projectCodexToClaude = (raw, input) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
86
|
+
return { continue: true };
|
|
87
|
+
const payload = raw;
|
|
88
|
+
const eventName = resolveEventName(payload, input);
|
|
89
|
+
const policy = eventName ? CODEX_OUTPUT_SCHEMA[eventName] : undefined;
|
|
90
|
+
// 8. 모르는 이벤트 → pass-through (continue 기본값만 보장)
|
|
91
|
+
if (!policy) {
|
|
92
|
+
const out = { ...payload };
|
|
93
|
+
if (typeof out.continue !== 'boolean')
|
|
94
|
+
out.continue = true;
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
const result = {};
|
|
98
|
+
// 2. universal
|
|
99
|
+
for (const k of policy.universal) {
|
|
100
|
+
const v = payload[k];
|
|
101
|
+
if (v !== undefined && v !== null)
|
|
102
|
+
result[k] = v;
|
|
103
|
+
}
|
|
104
|
+
if (policy.universal.includes('continue') && typeof result.continue !== 'boolean')
|
|
105
|
+
result.continue = true;
|
|
106
|
+
// 3. decision / reason
|
|
107
|
+
if (policy.decision) {
|
|
108
|
+
if (typeof payload.decision === 'string')
|
|
109
|
+
result.decision = payload.decision;
|
|
110
|
+
if (typeof payload.reason === 'string')
|
|
111
|
+
result.reason = payload.reason;
|
|
112
|
+
}
|
|
113
|
+
// 4. hookSpecificOutput — 허용 이벤트 + 허용 키만
|
|
114
|
+
const rawHso = payload.hookSpecificOutput;
|
|
115
|
+
if (policy.hso && rawHso && typeof rawHso === 'object') {
|
|
116
|
+
const filtered = { hookEventName: eventName };
|
|
117
|
+
for (const k of policy.hso) {
|
|
118
|
+
if (k === 'hookEventName')
|
|
119
|
+
continue;
|
|
120
|
+
const v = rawHso[k];
|
|
121
|
+
if (v !== undefined && v !== null)
|
|
122
|
+
filtered[k] = v;
|
|
74
123
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
124
|
+
// 5. PostToolUse: PreToolUse 형 deny → Codex PostToolUse block 형으로 번역
|
|
125
|
+
if (eventName === 'PostToolUse') {
|
|
126
|
+
const pd = rawHso.permissionDecision;
|
|
127
|
+
if (pd === 'deny' || pd === 'block') {
|
|
128
|
+
result.decision = 'block';
|
|
129
|
+
const pdr = rawHso.permissionDecisionReason;
|
|
130
|
+
if (typeof result.reason !== 'string' && typeof pdr === 'string')
|
|
131
|
+
result.reason = pdr;
|
|
132
|
+
}
|
|
83
133
|
}
|
|
134
|
+
result.hookSpecificOutput = filtered;
|
|
84
135
|
}
|
|
85
|
-
|
|
86
|
-
if (eventName) {
|
|
136
|
+
// 7. legacy approved boolean (PreToolUse 한정)
|
|
137
|
+
if (eventName === 'PreToolUse' && typeof payload.approved === 'boolean' && !result.hookSpecificOutput?.permissionDecision) {
|
|
87
138
|
result.hookSpecificOutput = {
|
|
88
139
|
hookEventName: eventName,
|
|
89
140
|
...(result.hookSpecificOutput ?? {}),
|
|
141
|
+
permissionDecision: payload.approved ? 'allow' : 'deny',
|
|
90
142
|
};
|
|
91
143
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
else {
|
|
100
|
-
result.hookSpecificOutput = {
|
|
101
|
-
...(result.hookSpecificOutput ?? {}),
|
|
102
|
-
permissionDecision: 'deny',
|
|
103
|
-
};
|
|
144
|
+
// 3b. block 은 non-empty reason 필수
|
|
145
|
+
if (typeof result.decision === 'string' && result.decision.toLowerCase() === 'block') {
|
|
146
|
+
const reason = typeof result.reason === 'string' ? result.reason.trim() : '';
|
|
147
|
+
if (!reason) {
|
|
148
|
+
result.reason = typeof result.systemMessage === 'string' && result.systemMessage.trim()
|
|
149
|
+
? result.systemMessage
|
|
150
|
+
: '[forgen] hook blocked this step; re-check the rule that fired before continuing.';
|
|
104
151
|
}
|
|
105
152
|
}
|
|
153
|
+
// 6. PreToolUse: continue:false 는 Codex 미지원 — permissionDecision 이 차단을 표현
|
|
154
|
+
if (eventName === 'PreToolUse' && result.continue === false && typeof result.hookSpecificOutput?.permissionDecision === 'string') {
|
|
155
|
+
result.continue = true;
|
|
156
|
+
}
|
|
106
157
|
return result;
|
|
107
158
|
};
|
|
108
159
|
/**
|
|
@@ -114,9 +165,20 @@ export const projectCodexToClaude = (raw, input) => {
|
|
|
114
165
|
* 교체하면 되도록 — `getProjection(host)` 가 단일 진입점.)
|
|
115
166
|
*/
|
|
116
167
|
export const projectClaudeToClaude = (raw, input) => projectCodexToClaude(raw, input);
|
|
168
|
+
/**
|
|
169
|
+
* OpenCode projection — P1 파운데이션 fail-loud 스텁.
|
|
170
|
+
* OpenCode 는 in-process plugin(throw/return)이라 subprocess stdout projection 과 형태가
|
|
171
|
+
* 다르다(plan §5 blocker 1). 실제 translation 은 plugin 슬림이 착지할 때 구현한다. 그 전까지
|
|
172
|
+
* 이 함수는 도달 불가(install-opencode 미구현 → getProjection('opencode') 미호출)이며,
|
|
173
|
+
* 혹시 호출되면 조용히 잘못된 결과를 내는 대신 명시적으로 실패한다.
|
|
174
|
+
*/
|
|
175
|
+
const projectOpencodeToClaude = () => {
|
|
176
|
+
throw new Error('[forgen] OpenCode projection 미구현 — P1 plugin 슬림 착지 후 구현 예정 (plan §4.1 in-process-plugin binding).');
|
|
177
|
+
};
|
|
117
178
|
const PROJECTIONS = {
|
|
118
179
|
claude: projectClaudeToClaude,
|
|
119
180
|
codex: projectCodexToClaude,
|
|
181
|
+
opencode: projectOpencodeToClaude,
|
|
120
182
|
};
|
|
121
183
|
export function getProjection(host) {
|
|
122
184
|
const fn = PROJECTIONS[host];
|
package/dist/mcp/tools.js
CHANGED
|
@@ -278,11 +278,24 @@ export function registerTools(server) {
|
|
|
278
278
|
try {
|
|
279
279
|
// v1 session_id를 환경변수에서 가져옴 (하네스가 설정)
|
|
280
280
|
const effectiveSessionId = session_id || process.env.FORGEN_SESSION_ID || 'unknown';
|
|
281
|
+
// W2-5 (private 태그): 교정 텍스트에서 <private>…</private>/forgen:private 범위
|
|
282
|
+
// 제거 후 저장. 교정 message 가 통째로 private 이면 저장 자체를 skip (학습
|
|
283
|
+
// 코퍼스에 남기지 않겠다는 사용자 의도). 조용히 하지 않고 공지한다.
|
|
284
|
+
const { stripPrivate, isFullyPrivate } = await import('../engine/private-filter.js');
|
|
285
|
+
if (isFullyPrivate(message)) {
|
|
286
|
+
return { content: [{ type: 'text', text: '✓ [forgen] correction skipped — message was fully <private> (not stored).' }] };
|
|
287
|
+
}
|
|
288
|
+
const cleanMessage = stripPrivate(message).cleaned;
|
|
289
|
+
const strippedTarget = stripPrivate(target);
|
|
290
|
+
const cleanTarget = strippedTarget.cleaned;
|
|
291
|
+
const privateNote = (strippedTarget.hadPrivate || cleanMessage !== message)
|
|
292
|
+
? '\n(참고: <private> 범위는 기록에서 제외됨)'
|
|
293
|
+
: '';
|
|
281
294
|
const result = processCorrection({
|
|
282
295
|
session_id: effectiveSessionId,
|
|
283
296
|
kind: kind,
|
|
284
|
-
message,
|
|
285
|
-
target,
|
|
297
|
+
message: cleanMessage,
|
|
298
|
+
target: cleanTarget,
|
|
286
299
|
axis_hint: axis_hint,
|
|
287
300
|
});
|
|
288
301
|
// Outcome tracking (Phase 1): attribute this correction to any
|
|
@@ -320,7 +333,7 @@ export function registerTools(server) {
|
|
|
320
333
|
return {
|
|
321
334
|
content: [{
|
|
322
335
|
type: 'text',
|
|
323
|
-
text: lines.join('\n'),
|
|
336
|
+
text: lines.join('\n') + privateNote,
|
|
324
337
|
}],
|
|
325
338
|
};
|
|
326
339
|
}
|
|
@@ -343,7 +356,7 @@ export function registerTools(server) {
|
|
|
343
356
|
return {
|
|
344
357
|
content: [{
|
|
345
358
|
type: 'text',
|
|
346
|
-
text: 'No profile configured. Run forgen onboarding.',
|
|
359
|
+
text: 'No profile configured. Run forgen forge --onboarding.',
|
|
347
360
|
}],
|
|
348
361
|
};
|
|
349
362
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Authoritative schema: docs/plans/2026-04-03-forgen-data-model-storage-spec.md §4
|
|
6
6
|
*/
|
|
7
7
|
import type { Evidence, EvidenceType } from './types.js';
|
|
8
|
+
import { type HostId } from '../core/trust-layer-intent.js';
|
|
8
9
|
export declare function createEvidence(params: {
|
|
9
10
|
type: EvidenceType;
|
|
10
11
|
session_id: string;
|
|
@@ -14,7 +15,7 @@ export declare function createEvidence(params: {
|
|
|
14
15
|
candidate_rule_refs?: string[];
|
|
15
16
|
confidence: number;
|
|
16
17
|
raw_payload?: Record<string, unknown>;
|
|
17
|
-
host?:
|
|
18
|
+
host?: HostId;
|
|
18
19
|
}): Evidence;
|
|
19
20
|
export declare function saveEvidence(evidence: Evidence): void;
|
|
20
21
|
/**
|
|
@@ -39,9 +40,4 @@ export declare function loadEvidenceByType(type: EvidenceType): Evidence[];
|
|
|
39
40
|
export declare function loadRecentEvidence(limit?: number): Evidence[];
|
|
40
41
|
/** prefer-from-now / avoid-this 교정 evidence를 모두 반환 (규칙 승격 후보) */
|
|
41
42
|
export declare function loadPromotionCandidates(): Evidence[];
|
|
42
|
-
/**
|
|
43
|
-
* 특정 세션의 promotion 후보를 scope:'me' 영구 규칙으로 승격.
|
|
44
|
-
* 동일 render_key를 가진 scope:'me' 규칙이 이미 있으면 건너뜀.
|
|
45
|
-
* @returns 승격된 규칙 수
|
|
46
|
-
*/
|
|
47
43
|
export declare function promoteSessionCandidates(sessionId: string): number;
|
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import * as crypto from 'node:crypto';
|
|
10
|
-
import { ME_BEHAVIOR } from '../core/paths.js';
|
|
10
|
+
import { ME_BEHAVIOR, ME_RULES } from '../core/paths.js';
|
|
11
11
|
import { atomicWriteJSON, safeReadJSON } from '../hooks/shared/atomic-write.js';
|
|
12
|
-
import {
|
|
12
|
+
import { HOST_IDS } from '../core/trust-layer-intent.js';
|
|
13
|
+
import { createRule, saveRule, loadActiveRules, updateRuleStatus, findRuleByRenderKey } from './rule-store.js';
|
|
13
14
|
import { classify, applyProposal } from '../engine/enforce-classifier.js';
|
|
14
15
|
import { detect as detectT1 } from '../engine/lifecycle/trigger-t1-correction.js';
|
|
15
16
|
import { foldEvents } from '../engine/lifecycle/orchestrator.js';
|
|
16
17
|
import { appendLifecycleEvents } from '../engine/lifecycle/meta-reclassifier.js';
|
|
18
|
+
import { laplaceConfidence, strengthForConfidence } from '../engine/correction-clustering.js';
|
|
19
|
+
import { withFileLockSync } from '../hooks/shared/file-lock.js';
|
|
17
20
|
function evidencePath(evidenceId) {
|
|
18
21
|
return path.join(ME_BEHAVIOR, `${evidenceId}.json`);
|
|
19
22
|
}
|
|
@@ -32,11 +35,13 @@ function evidencePath(evidenceId) {
|
|
|
32
35
|
function detectHost(explicit) {
|
|
33
36
|
if (explicit)
|
|
34
37
|
return explicit;
|
|
38
|
+
// W3-3 리뷰 SEV-3 #5: 이진비교(=== 'claude' || === 'codex') 대신 HOST_IDS 정준목록으로
|
|
39
|
+
// 유효 host 를 해소해 FORGEN_HOST/FORGEN_RUNTIME='opencode' 도 보존(self-evidence-record 일관).
|
|
35
40
|
const fromEnv = process.env.FORGEN_HOST;
|
|
36
|
-
if (fromEnv
|
|
41
|
+
if (fromEnv && HOST_IDS.includes(fromEnv))
|
|
37
42
|
return fromEnv;
|
|
38
43
|
const fromRuntime = process.env.FORGEN_RUNTIME;
|
|
39
|
-
if (fromRuntime
|
|
44
|
+
if (fromRuntime && HOST_IDS.includes(fromRuntime))
|
|
40
45
|
return fromRuntime;
|
|
41
46
|
if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX_NETWORK_DISABLED)
|
|
42
47
|
return 'codex';
|
|
@@ -111,13 +116,17 @@ export function appendEvidence(evidence) {
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
/**
|
|
114
|
-
* 기존 evidence 에 host 필드가
|
|
115
|
-
* 새 multi-host 도입 이전 데이터는 모두 Claude
|
|
119
|
+
* 기존 evidence 에 host 필드가 없거나 유효하지 않으면 'claude' 로 backfill
|
|
120
|
+
* (Multi-Host §4.2 마이그레이션 정책). 새 multi-host 도입 이전 데이터는 모두 Claude 발생.
|
|
121
|
+
*
|
|
122
|
+
* W3-3 리뷰 SEV-3 #5: 이전엔 `!== 'claude' && !== 'codex'` 라 **유효한 'opencode' 태그도
|
|
123
|
+
* claude 로 clobber** → self-evidence-record='supported'(host:"opencode") 선언과 정면 모순.
|
|
124
|
+
* 이제 HOST_IDS 정준 목록으로 "유효 host" 를 판정해 opencode 등 모든 유효 태그를 보존한다.
|
|
116
125
|
*/
|
|
117
126
|
function backfillHost(ev) {
|
|
118
127
|
if (!ev)
|
|
119
128
|
return ev;
|
|
120
|
-
if (ev.host
|
|
129
|
+
if (ev.host && HOST_IDS.includes(ev.host))
|
|
121
130
|
return ev;
|
|
122
131
|
return { ...ev, host: 'claude' };
|
|
123
132
|
}
|
|
@@ -163,45 +172,154 @@ export function loadPromotionCandidates() {
|
|
|
163
172
|
* 동일 render_key를 가진 scope:'me' 규칙이 이미 있으면 건너뜀.
|
|
164
173
|
* @returns 승격된 규칙 수
|
|
165
174
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
trigger: target,
|
|
190
|
-
policy: candidate.summary,
|
|
191
|
-
strength: kind === 'avoid-this' ? 'strong' : 'default',
|
|
192
|
-
source: 'explicit_correction',
|
|
193
|
-
evidence_refs: [candidate.evidence_id],
|
|
194
|
-
render_key: renderKey,
|
|
195
|
-
});
|
|
196
|
-
// ADR-001 auto-classify — 승격되는 rule 에도 enforce_via 자동 주입.
|
|
197
|
-
try {
|
|
198
|
-
const proposal = classify(rule);
|
|
199
|
-
rule = applyProposal(rule, proposal);
|
|
175
|
+
/**
|
|
176
|
+
* ADR-013 채굴 교정 안전 파라미터 (리뷰 SEV-1 대응):
|
|
177
|
+
* - 채굴 룰은 "auto:" render_key 네임스페이스로 분리 → 실시간 명시 교정과 절대 충돌/억제 안 함.
|
|
178
|
+
* - advisory-only: enforce_via=[] (차단 메커니즘 미부여). 환각 교정이 차단 룰이 되는 것 근절.
|
|
179
|
+
* - 생성나이 은퇴: TTL 지난 미확인 채굴 룰 retire (me-scope 룰이 매 세션 주입돼 ROI/T4 로는
|
|
180
|
+
* 은퇴 안 되던 문제의 실제 해소). + 라이브 채굴 룰 총량 상한.
|
|
181
|
+
*/
|
|
182
|
+
const AUTO_MINED_PREFIX = 'auto:';
|
|
183
|
+
const AUTO_MINED_TTL_MS = 21 * 24 * 60 * 60 * 1000; // 21일
|
|
184
|
+
const AUTO_MINED_MAX_LIVE = 30;
|
|
185
|
+
/** 채굴(behavior_inference) 룰 은퇴: TTL 초과 + 총량 상한 초과분(오래된 순). */
|
|
186
|
+
function retireStaleAutoMinedRules(now) {
|
|
187
|
+
const auto = loadActiveRules().filter((r) => r.scope === 'me' && r.source === 'behavior_inference' && r.render_key?.startsWith(AUTO_MINED_PREFIX));
|
|
188
|
+
const byAge = auto
|
|
189
|
+
.map((r) => ({ r, age: now - Date.parse(r.created_at) }))
|
|
190
|
+
.sort((a, b) => b.age - a.age); // 오래된 순
|
|
191
|
+
const keep = [];
|
|
192
|
+
for (const item of byAge) {
|
|
193
|
+
if (item.age >= AUTO_MINED_TTL_MS) {
|
|
194
|
+
updateRuleStatus(item.r.rule_id, 'removed'); // TTL 초과 → 은퇴
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
keep.push(item);
|
|
200
198
|
}
|
|
201
|
-
catch { /* fail-open */ }
|
|
202
|
-
saveRule(rule);
|
|
203
|
-
existingRenderKeys.add(renderKey);
|
|
204
|
-
promoted++;
|
|
205
199
|
}
|
|
206
|
-
|
|
200
|
+
// 총량 상한 초과분(가장 오래된 것부터) 은퇴.
|
|
201
|
+
const excess = keep.length - AUTO_MINED_MAX_LIVE;
|
|
202
|
+
for (let i = 0; i < excess; i++)
|
|
203
|
+
updateRuleStatus(keep[i].r.rule_id, 'removed');
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* retire+dedupe+create 시퀀스 전체를 잠그는 lock 타깃. 실제 파일은 만들지 않고
|
|
207
|
+
* (`${target}.lock`) 파생 lock 파일만 생성 — withFileLockSync 관례.
|
|
208
|
+
* RCA (C): 여러 detached auto-compound-runner 가 동시에 이 함수를 돌리면 각자
|
|
209
|
+
* "render_key 없음" 스냅샷을 보고 중복 rule 을 만들 수 있었다(TOCTOU). retire ~
|
|
210
|
+
* create 를 하나의 critical section 으로 묶어 그 창을 없앤다.
|
|
211
|
+
*/
|
|
212
|
+
const RULE_STORE_MUTATE_LOCK = path.join(ME_RULES, '.rule-store-mutate');
|
|
213
|
+
export function promoteSessionCandidates(sessionId) {
|
|
214
|
+
// withFileLockSync 는 O_CREAT 로 lock 파일을 만들 뿐 부모 디렉터리는 만들지 않는다 —
|
|
215
|
+
// 첫 rule 승급 이전(=아직 ME_RULES 없음)에도 락을 잡을 수 있어야 하므로 선행 mkdir.
|
|
216
|
+
fs.mkdirSync(ME_RULES, { recursive: true });
|
|
217
|
+
// 락 타임아웃/보수 시간은 이 critical section 이 짧다는 전제(디렉터리 스캔 + 파일
|
|
218
|
+
// 몇 개 쓰기, 초 단위 아님) 로 file-lock.ts 기본값(2s/30s)보다 넉넉히 잡되, 죽은
|
|
219
|
+
// holder 를 오래 방치하지 않도록 stale 은 짧게 유지한다.
|
|
220
|
+
return withFileLockSync(RULE_STORE_MUTATE_LOCK, () => {
|
|
221
|
+
// 채굴 룰 TTL 은퇴는 **early-return 앞에서** 실행 (critic 재검증 #1): 승급 후보가 없는
|
|
222
|
+
// 조용한 세션에서도 wall-clock TTL 이 실제로 강제되도록. (auto-compound-runner 는 매
|
|
223
|
+
// 세션 종료 시 promoteSessionCandidates 를 호출하므로 이 경로가 세션당 1회 보장된다.)
|
|
224
|
+
retireStaleAutoMinedRules(Date.now());
|
|
225
|
+
const candidates = loadPromotionCandidates().filter(e => e.session_id === sessionId);
|
|
226
|
+
if (candidates.length === 0)
|
|
227
|
+
return 0;
|
|
228
|
+
let promoted = 0;
|
|
229
|
+
for (const candidate of candidates) {
|
|
230
|
+
const payload = candidate.raw_payload;
|
|
231
|
+
const axisHint = payload?.axis_hint;
|
|
232
|
+
const target = payload?.target;
|
|
233
|
+
const kind = payload?.kind;
|
|
234
|
+
const autoMined = payload?.auto_mined === true;
|
|
235
|
+
if (!target)
|
|
236
|
+
continue;
|
|
237
|
+
// 채굴 룰은 "auto:" 네임스페이스 — 실시간 교정 render_key 와 절대 충돌 안 함 (SEV-2 #4).
|
|
238
|
+
const baseKey = `${axisHint ?? 'workflow'}.${target.toLowerCase().replace(/\s+/g, '-').slice(0, 30)}`;
|
|
239
|
+
const renderKey = autoMined ? `${AUTO_MINED_PREFIX}${baseKey}` : baseKey;
|
|
240
|
+
const source = autoMined ? 'behavior_inference' : 'explicit_correction';
|
|
241
|
+
// (B) 이 evidence 가 과거 어떤 rule 로든 이미 소비됐는가 — candidate_rule_refs 를
|
|
242
|
+
// "승급 완료" 마커로 재사용한다(T1 이 rule_id 매칭에 쓰는 필드와 동일 필드,
|
|
243
|
+
// 의미상 호환: "이 evidence 가 가리키는 rule"). loadPromotionCandidates() 는
|
|
244
|
+
// evidence 를 영구히 반환하므로, 이 마커가 없으면 재-sweep 마다 같은 evidence 가
|
|
245
|
+
// 무한 재처리된다.
|
|
246
|
+
const alreadyConsumed = (candidate.candidate_rule_refs ?? []).length > 0;
|
|
247
|
+
// (A) render_key upsert identity — status 무관 기존 rule 조회. TTL/cap 로
|
|
248
|
+
// retire(status='removed')된 render_key 도 여기서 발견되어 "재생성"이 아니라
|
|
249
|
+
// "재활성화"된다 — 새 rule_id/created_at 을 절대 새로 만들지 않는다.
|
|
250
|
+
// scope:'me' 로 한정 — processCorrection() 이 fix-now/avoid-this 에 대해 만드는
|
|
251
|
+
// scope:'session' 임시 rule 은 같은 render_key 공식을 쓰지만 별개 개체다(승격 대상 아님).
|
|
252
|
+
const existingRule = findRuleByRenderKey(renderKey, source, 'me');
|
|
253
|
+
if (existingRule) {
|
|
254
|
+
if (alreadyConsumed)
|
|
255
|
+
continue; // 같은 evidence 의 sweep 재관측 — no-op.
|
|
256
|
+
const evidenceRefs = existingRule.evidence_refs.includes(candidate.evidence_id)
|
|
257
|
+
? existingRule.evidence_refs
|
|
258
|
+
: [...existingRule.evidence_refs, candidate.evidence_id];
|
|
259
|
+
// (D1) confidence 재계산 — correction-clustering.ts 의 Laplace 계승법칙을 그대로
|
|
260
|
+
// 상속(리터럴 복제 아님). N=evidence_refs.length(=관측 반복 횟수). auto_mined 은
|
|
261
|
+
// ADR-013 불변식(advisory-only, strong 절대 자동 도달 금지)을 유지하기 위해
|
|
262
|
+
// 몇 번을 재확인해도 'default' 를 벗어나지 않는다. hard 는 confidence 로 낮추지
|
|
263
|
+
// 않는다(안전 룰은 강도 하락이 없어야 함).
|
|
264
|
+
const strength = autoMined
|
|
265
|
+
? 'default'
|
|
266
|
+
: existingRule.strength === 'hard'
|
|
267
|
+
? 'hard'
|
|
268
|
+
: strengthForConfidence(laplaceConfidence(evidenceRefs.length));
|
|
269
|
+
// TTL-on-reactivation 결정: created_at 은 절대 now 로 리셋하지 않는다(carry-forward).
|
|
270
|
+
// 이유 — AUTO_MINED_TTL_MS 는 "이 개념이 처음 채굴된 이후 최대 수명"을 뜻하는
|
|
271
|
+
// 하드 상한이지, "마지막으로 확인된 이후 경과 시간"이 아니다. created_at 을
|
|
272
|
+
// 리셋하면 반복 채굴(같은 세션이 오래 지속되며 매 sweep 마다 재-mine)로 TTL 을
|
|
273
|
+
// 무한 연장할 수 있어 은퇴 메커니즘 자체가 무력화된다(원 결함의 재발). 대신
|
|
274
|
+
// rule_id/created_at 은 그대로 두고 status 만 'active' 로 되돌린다 — 재활성화된
|
|
275
|
+
// rule 은 다음 retireStaleAutoMinedRules() 호출 때 기존 나이 기준으로 다시
|
|
276
|
+
// 은퇴 여부가 평가된다(의도된 동작, thrash 아님: 최소 1세션은 유효하게 주입됨).
|
|
277
|
+
saveRule({ ...existingRule, status: 'active', evidence_refs: evidenceRefs, strength });
|
|
278
|
+
saveEvidence({
|
|
279
|
+
...candidate,
|
|
280
|
+
candidate_rule_refs: [...(candidate.candidate_rule_refs ?? []), existingRule.rule_id],
|
|
281
|
+
});
|
|
282
|
+
promoted++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (alreadyConsumed)
|
|
286
|
+
continue; // 소비 마킹된 evidence 인데 매칭 rule 없음(이례) — 재생성 금지.
|
|
287
|
+
const category = axisHint === 'quality_safety' ? 'quality'
|
|
288
|
+
: axisHint === 'autonomy' ? 'autonomy'
|
|
289
|
+
: 'workflow';
|
|
290
|
+
// ADR-013: 채굴 교정(auto_mined)은 실시간 명시 교정과 엄격 차등 — provenance
|
|
291
|
+
// =behavior_inference, strength 절대 strong 아님(default).
|
|
292
|
+
let rule = createRule({
|
|
293
|
+
category,
|
|
294
|
+
scope: 'me',
|
|
295
|
+
trigger: target,
|
|
296
|
+
policy: candidate.summary,
|
|
297
|
+
strength: autoMined ? 'default' : kind === 'avoid-this' ? 'strong' : 'default',
|
|
298
|
+
source,
|
|
299
|
+
evidence_refs: [candidate.evidence_id],
|
|
300
|
+
render_key: renderKey,
|
|
301
|
+
});
|
|
302
|
+
if (autoMined) {
|
|
303
|
+
// advisory-only (SEV-1 #2): classify() 를 돌리지 않고 enforce_via 를 비워, 채굴 룰이
|
|
304
|
+
// 절대 Mech-A 차단(PreToolUse/Stop block)을 얻지 못하게 한다. 컨텍스트 주입만 되고
|
|
305
|
+
// 어떤 훅도 강제하지 않음 — 환각 교정이 영구 차단 룰이 되는 위험 근절.
|
|
306
|
+
rule.enforce_via = [];
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// ADR-001 auto-classify — 실시간 승격 rule 에만 enforce_via 자동 주입.
|
|
310
|
+
try {
|
|
311
|
+
const proposal = classify(rule);
|
|
312
|
+
rule = applyProposal(rule, proposal);
|
|
313
|
+
}
|
|
314
|
+
catch { /* fail-open */ }
|
|
315
|
+
}
|
|
316
|
+
saveRule(rule);
|
|
317
|
+
saveEvidence({
|
|
318
|
+
...candidate,
|
|
319
|
+
candidate_rule_refs: [...(candidate.candidate_rule_refs ?? []), rule.rule_id],
|
|
320
|
+
});
|
|
321
|
+
promoted++;
|
|
322
|
+
}
|
|
323
|
+
return promoted;
|
|
324
|
+
}, { timeoutMs: 8000, staleMs: 15000 });
|
|
207
325
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - drift_event_count_by_host
|
|
12
12
|
* 위 3 종 metric 을 evidence_id → host 매핑으로 집계.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
import type { HostId } from '../core/trust-layer-intent.js';
|
|
15
15
|
export interface HostMismatchSummary {
|
|
16
16
|
readonly byHost: Record<HostId, number>;
|
|
17
17
|
readonly total: number;
|
|
@@ -33,6 +33,7 @@ export declare function summarizeNegativeSignalsForRef(refId: string): HostMisma
|
|
|
33
33
|
export declare function summarizeAllByHost(): {
|
|
34
34
|
claude: number;
|
|
35
35
|
codex: number;
|
|
36
|
+
opencode: number;
|
|
36
37
|
total: number;
|
|
37
38
|
};
|
|
38
39
|
/** 테스트 노출용 — 임계값이 변경되면 회귀 즉시 감지. */
|