@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
|
@@ -5,11 +5,21 @@
|
|
|
5
5
|
* 단일 소스로 통합. 한쪽만 고치면 다른 쪽이 drift 하는 sibling-bug 패턴 차단.
|
|
6
6
|
*
|
|
7
7
|
* 설계 결정:
|
|
8
|
-
* - trigger 는 명시적 완료 선언 동사/어미만 — "완료" 단독 매칭 금지 (retraction 오매칭 방지).
|
|
8
|
+
* - DEFAULT trigger 는 명시적 완료 선언 동사/어미만 — "완료" 단독 매칭 금지 (retraction 오매칭 방지).
|
|
9
9
|
* - exclude 는 retraction/negation/meta 언급 광범위 차단.
|
|
10
10
|
* - A1 spike 결과로 검증됨 (10/10 scenarios pass, FP 0%).
|
|
11
|
+
*
|
|
12
|
+
* 2026-07-22 (강제층 실측 갭 — critic-review 룰 한정 수정, 리뷰 SEV-2 반영):
|
|
13
|
+
* 완료 키워드 없이 "리뷰 생략하고 다음으로 넘어감" 하는 응답이 완료-키워드 트리거를
|
|
14
|
+
* 우회해 critic-룰(청크마다 리뷰) 강제가 새던 갭. 이를 닫되:
|
|
15
|
+
* - skip-signal 은 DEFAULT 에 섞지 않고 **critic-review 룰 전용** CRITIC_STOP_TRIGGER 로 분리
|
|
16
|
+
* (e2e·mock-as-proof 등 다른 완료룰 오염 방지, 리뷰 SEV-2 #3).
|
|
17
|
+
* - "리뷰 생략" AND "다음으로 넘어감" **결합(conjunction)** 으로만 발화 → 경고/질문/신중
|
|
18
|
+
* 응답 FP 제거 (리뷰 SEV-2 #1).
|
|
19
|
+
* - exclude 에 부정/금지/질문형(말고·안 했·should not·될까요…) 보강 (리뷰 SEV-2 #2).
|
|
20
|
+
* - 기존 baked 룰(b0aabac3)은 `forgen rule migrate-triggers` 로 재-bake 해야 적용 (리뷰 SEV-2 #4).
|
|
11
21
|
*/
|
|
12
|
-
/** Stop hook
|
|
22
|
+
/** Stop hook 기본 완료 선언 매칭 (완료 동사/어미만 — skip-signal 미포함). */
|
|
13
23
|
export const DEFAULT_STOP_TRIGGER_RE = '(완료했|완성됐|완성되|완성했|done\\.|ready\\.|shipped\\.|LGTM|finished\\.)';
|
|
14
24
|
/** Stop hook 기본 exclude — retraction/negation/meta 맥락 제외. */
|
|
15
25
|
export const DEFAULT_STOP_EXCLUDE_RE = '(취소|철회|없음|없습니다|않았|하지\\s*않|아닙니다|not\\s*yet|no\\s*longer|retract|withdraw|아직\\s*(안|아))';
|
|
@@ -17,3 +27,26 @@ export const DEFAULT_STOP_EXCLUDE_RE = '(취소|철회|없음|없습니다|않
|
|
|
17
27
|
export const MOCK_TRIGGER_RE = '(mock|stub|fake)';
|
|
18
28
|
/** mock trigger 의 exclude — 테스트 맥락은 정상. */
|
|
19
29
|
export const MOCK_EXCLUDE_RE = '(테스트|test|vi\\.mock|jest\\.mock|spec\\.)';
|
|
30
|
+
// ── critic-review 룰 전용 트리거 (2026-07-22) ─────────────────────────────────
|
|
31
|
+
/** "리뷰/검토 생략" 시그널 (한/영). 외래어(스킵/패스)·구어(안 하고) 포함 — 리뷰 SEV-3 (a). */
|
|
32
|
+
const REVIEW_SKIP = '(리뷰|검토)[^.!?\\n]{0,8}(생략|건너뛰|없이|스킵|패스|안\\s*하고)|skip(?:ping|s)?\\s+(?:the\\s+)?review';
|
|
33
|
+
/** "다음으로 넘어감" 진행 시그널 (활용형 커버). */
|
|
34
|
+
const MOVE_ON = '넘어가|넘어갑|넘어갔|넘어감|넘어갈|다음\\s*(작업|기능|단계|스텝|것|이터레이션)|move\\s+on|next\\s+(step|task|feature)|proceed';
|
|
35
|
+
/**
|
|
36
|
+
* "리뷰 생략" AND "다음으로 넘어감" 이 함께 있을 때만 발화하는 결합 트리거.
|
|
37
|
+
* 두 lookahead 로 conjunction — 한쪽만 있는 경고/질문/신중 응답은 미발화(FP 0).
|
|
38
|
+
*/
|
|
39
|
+
export const SKIP_REVIEW_TRIGGER_RE = `(?=[\\s\\S]*?(?:${REVIEW_SKIP}))(?=[\\s\\S]*?(?:${MOVE_ON}))`;
|
|
40
|
+
/**
|
|
41
|
+
* critic-review 룰 트리거 = 완료 선언 OR 리뷰생략-넘어감.
|
|
42
|
+
* (완료 시점 + skip 시점 양쪽에서 critic 강제. e2e/mock 룰엔 부여 안 함 — DEFAULT 만 사용.)
|
|
43
|
+
*/
|
|
44
|
+
export const CRITIC_STOP_TRIGGER_RE = `(${DEFAULT_STOP_TRIGGER_RE}|${SKIP_REVIEW_TRIGGER_RE})`;
|
|
45
|
+
/**
|
|
46
|
+
* critic 트리거 exclude — 기본 retraction + 부정/금지/질문형 + 숙고형 보강.
|
|
47
|
+
* 주의: bare `안\s*하` 는 넣지 않는다 — "리뷰 안 하고 넘어감"(실제 skip TP)을 죽이므로.
|
|
48
|
+
* retraction 은 과거형 `안\s*했`(안 했다)만 배제. 숙고형(할지|여부|고민…)은 skip 단언이
|
|
49
|
+
* 아니라 결정 전 단계라 배제 (리뷰 SEV-3 (a)).
|
|
50
|
+
*/
|
|
51
|
+
export const CRITIC_STOP_EXCLUDE_RE = '(?:' + DEFAULT_STOP_EXCLUDE_RE +
|
|
52
|
+
'|말고|마세요|말라|안\\s*했|안\\s*해|안\\s*할|안\\s*돼|안\\s*됩|하세요|반드시|위험|should\\s*not|shouldn|don.?t|될까요|까요\\?|할지|갈지|여부|고민|결정하)';
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
* If fitness data is unavailable (fresh install, empty outcomes/),
|
|
26
26
|
* every solution falls into the default 0.3 bucket — identical to the
|
|
27
27
|
* pre-0.3.2 gate. No cold-start regression.
|
|
28
|
+
*
|
|
29
|
+
* W3-2 리뷰(SEV-3 #1): 0.3 리터럴을 relevance-gate.RELEVANCE_MATCH_GATE 로 단일화 —
|
|
30
|
+
* 교정 클러스터링 τ 와 같은 소스를 공유해 근거-정직성 드리프트를 코드로 방지.
|
|
28
31
|
*/
|
|
29
32
|
export declare const MIN_INJECT_RELEVANCE = 0.3;
|
|
30
33
|
export declare const MIN_INJECT_RELEVANCE_TRUSTED = 0.25;
|
|
@@ -85,4 +88,29 @@ export declare function commitSessionCacheEntries(sessionId: string, newEntries:
|
|
|
85
88
|
name: string;
|
|
86
89
|
chars: number;
|
|
87
90
|
}>): SessionCacheCommitResult;
|
|
91
|
+
interface TierSolution {
|
|
92
|
+
name: string;
|
|
93
|
+
type: string;
|
|
94
|
+
confidence: number;
|
|
95
|
+
matchedTags: string[];
|
|
96
|
+
}
|
|
97
|
+
/** Tier 2 — 헤더 + 태그 + 핵심 최대 3줄 (최대 SUMMARY_MAX_CHARS) */
|
|
98
|
+
export declare function buildSummaryTier(sol: TierSolution, raw: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Tier 1 — 이름 + one-liner + compound-read MCP 힌트 (전문은 pull-on-demand).
|
|
101
|
+
* type/confidence 헤더는 의도적으로 생략 — 인덱스 라인은 포인터일 뿐, 상세는
|
|
102
|
+
* compound-read로 당겨 읽는다.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildIndexTier(sol: TierSolution, raw: string): string;
|
|
105
|
+
/** Tier 0 — 전문(Context+Content) 그대로. 파싱 실패/캡 초과면 null (호출부가 Tier 2로 폴백). */
|
|
106
|
+
export declare function buildFullTier(sol: TierSolution, raw: string): string | null;
|
|
107
|
+
/**
|
|
108
|
+
* 매치 목록(relevance desc 정렬 가정)을 Progressive Disclosure 텍스트로 렌더한다.
|
|
109
|
+
* 순수 함수 — fs 읽기는 caller(main)가 `rawByName`으로 미리 채워 넣는다.
|
|
110
|
+
*
|
|
111
|
+
* - 매치가 정확히 1건 & 전문이 FULL_TEXT_MAX_CHARS 이하 → Tier 0(전문)
|
|
112
|
+
* - 그 외 상위 TOP_TIER_COUNT건 → Tier 2(요약)
|
|
113
|
+
* - 나머지 → Tier 1(인덱스 라인)
|
|
114
|
+
*/
|
|
115
|
+
export declare function renderSolutionTiers(sols: readonly TierSolution[], rawByName: ReadonlyMap<string, string>): Map<string, string>;
|
|
88
116
|
export {};
|
|
@@ -12,6 +12,7 @@ import * as path from 'node:path';
|
|
|
12
12
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
13
13
|
import { isHookEnabled } from './hook-config.js';
|
|
14
14
|
import { matchSolutions } from '../engine/solution-matcher.js';
|
|
15
|
+
import { RELEVANCE_MATCH_GATE } from '../engine/relevance-gate.js';
|
|
15
16
|
import { extractTags } from '../engine/solution-format.js';
|
|
16
17
|
import { defaultNormalizer } from '../engine/term-normalizer.js';
|
|
17
18
|
import { logMatchDecision } from '../engine/match-eval-log.js';
|
|
@@ -26,6 +27,7 @@ import { withFileLock, withFileLockSync, FileLockError } from './shared/file-loc
|
|
|
26
27
|
import { calculateBudget } from './shared/context-budget.js';
|
|
27
28
|
import { writeSignal } from './shared/plugin-signal.js';
|
|
28
29
|
import { approve, approveWithContext, failOpenWithTracking } from './shared/hook-response.js';
|
|
30
|
+
import { truncateContent } from './shared/injection-caps.js';
|
|
29
31
|
import { STATE_DIR } from '../core/paths.js';
|
|
30
32
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
31
33
|
import { appendPending, flushAccept } from '../engine/solution-outcomes.js';
|
|
@@ -52,8 +54,11 @@ const MAX_SOLUTIONS_PER_SESSION = 10;
|
|
|
52
54
|
* If fitness data is unavailable (fresh install, empty outcomes/),
|
|
53
55
|
* every solution falls into the default 0.3 bucket — identical to the
|
|
54
56
|
* pre-0.3.2 gate. No cold-start regression.
|
|
57
|
+
*
|
|
58
|
+
* W3-2 리뷰(SEV-3 #1): 0.3 리터럴을 relevance-gate.RELEVANCE_MATCH_GATE 로 단일화 —
|
|
59
|
+
* 교정 클러스터링 τ 와 같은 소스를 공유해 근거-정직성 드리프트를 코드로 방지.
|
|
55
60
|
*/
|
|
56
|
-
export const MIN_INJECT_RELEVANCE =
|
|
61
|
+
export const MIN_INJECT_RELEVANCE = RELEVANCE_MATCH_GATE;
|
|
57
62
|
export const MIN_INJECT_RELEVANCE_TRUSTED = 0.25;
|
|
58
63
|
/**
|
|
59
64
|
* v0.4.1 — cold-start 사용자 threshold. outcomes 이벤트가 거의 없는 신규 사용자
|
|
@@ -305,6 +310,111 @@ async function detectActOnFromPriorSurface(sessionId, promptLower) {
|
|
|
305
310
|
log.debug('detectActOnFromPriorSurface 실패', e);
|
|
306
311
|
}
|
|
307
312
|
}
|
|
313
|
+
// ── Progressive Disclosure tiers (claude-mem 스타일 index→summary→full 대응) ──
|
|
314
|
+
//
|
|
315
|
+
// forgen은 이미 Tier 3(전문)를 compound-read MCP tool로 갖고 있었다 (fs 안 읽고
|
|
316
|
+
// on-demand pull). 이번 변경은 Tier 0~2 배분만 다시 나눈다: 매치가 여러 건일 때
|
|
317
|
+
// 상위 랭크만 두껍게(Tier 2 요약) 주입하고, 나머지는 포인터 한 줄(Tier 1 인덱스)만
|
|
318
|
+
// 준다 — claude-mem이 상위만 전개하고 나머지는 인덱스로 남겨 ~10x 토큰을 아끼는
|
|
319
|
+
// 것과 같은 원리. 매치가 정확히 1건이면 굳이 요약할 필요가 없으므로 전문이
|
|
320
|
+
// 작으면(Tier 0) 통째로 준다.
|
|
321
|
+
//
|
|
322
|
+
// 상위 몇 건이 Tier 2를 받는지 — 세션당 솔루션 수(2~3)를 감안해 1건만 두껍게 두면
|
|
323
|
+
// 3건 매치 시나리오에서 인덱스 라인 2개로 대체되어 눈에 띄는 절감이 난다.
|
|
324
|
+
const TOP_TIER_COUNT = 1;
|
|
325
|
+
/** Tier 2(요약 블록): 헤더 + 핵심 최대 3줄. 이전 "Tier 2.5"와 동일한 예산 유지. */
|
|
326
|
+
const SUMMARY_MAX_CHARS = 300;
|
|
327
|
+
/**
|
|
328
|
+
* Tier 1(인덱스 라인): one-liner 부분만의 캡 — 힌트는 별도.
|
|
329
|
+
* 인덱스 라인은 의도적으로 type/confidence 헤더를 생략한다(포인터 성격 —
|
|
330
|
+
* 상세는 compound-read pull). 3건 매치 시 "1×Tier2 + N×Tier1"이 구 방식
|
|
331
|
+
* "N×Tier2" 대비 ≥50% 절감되도록 하는 예산의 핵심 변수.
|
|
332
|
+
*/
|
|
333
|
+
const INDEX_ONE_LINER_MAX_CHARS = 50;
|
|
334
|
+
/**
|
|
335
|
+
* Tier 0(전문): 매치가 정확히 1건이고 전문(Context+Content)이 이 캡 이하일 때만
|
|
336
|
+
* 요약 대신 전문을 그대로 준다. solutionMax(1500, injection-caps.ts) 보다 낮춰
|
|
337
|
+
* 세션 누적 버짓에 여유를 남긴다.
|
|
338
|
+
*/
|
|
339
|
+
const FULL_TEXT_MAX_CHARS = 1200;
|
|
340
|
+
/**
|
|
341
|
+
* 한 턴(UserPromptSubmit) 주입 전체의 최종 안전판. 세션 누적 캡
|
|
342
|
+
* (context-budget.ts의 solutionSessionMax, 기본 8000)과는 별개 축 — 그건
|
|
343
|
+
* "세션 전체 합"이고 이건 "이번 한 번의 additionalContext 크기" 상한이다.
|
|
344
|
+
* 매칭/렌더 로직에 버그가 생겨도 한 턴이 컨텍스트를 과점하지 못하게 막는
|
|
345
|
+
* 최후 backstop (경쟁 리포트 §5-2 기준값과 정합).
|
|
346
|
+
*/
|
|
347
|
+
const INJECTION_HARD_CAP_CHARS = 4000;
|
|
348
|
+
/** `## Content` 섹션 본문만 추출 (없으면 빈 문자열) */
|
|
349
|
+
function extractContentSection(raw) {
|
|
350
|
+
const contentMatch = raw.match(/## Content\n([\s\S]*?)(?:\n## |\n---|$)/);
|
|
351
|
+
return contentMatch ? contentMatch[1] : '';
|
|
352
|
+
}
|
|
353
|
+
/** 코드블록 제거 + 트림 + 빈 줄 제거된 본문 줄 목록 */
|
|
354
|
+
function contentLines(raw) {
|
|
355
|
+
return extractContentSection(raw)
|
|
356
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
357
|
+
.split('\n')
|
|
358
|
+
.map(l => l.trim())
|
|
359
|
+
.filter(l => l.length > 0);
|
|
360
|
+
}
|
|
361
|
+
function tierHeader(sol) {
|
|
362
|
+
return `${sol.name} [${sol.type}|${sol.confidence.toFixed(2)}]`;
|
|
363
|
+
}
|
|
364
|
+
/** Tier 2 — 헤더 + 태그 + 핵심 최대 3줄 (최대 SUMMARY_MAX_CHARS) */
|
|
365
|
+
export function buildSummaryTier(sol, raw) {
|
|
366
|
+
let snippet = contentLines(raw).slice(0, 3).join('\n');
|
|
367
|
+
if (snippet.length > SUMMARY_MAX_CHARS) {
|
|
368
|
+
snippet = `${snippet.slice(0, SUMMARY_MAX_CHARS - 3)}...`;
|
|
369
|
+
}
|
|
370
|
+
const header = `${tierHeader(sol)}: ${sol.matchedTags.slice(0, 5).join(', ')}`;
|
|
371
|
+
return snippet ? `${header}\n ${snippet.replace(/\n/g, '\n ')}` : header;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Tier 1 — 이름 + one-liner + compound-read MCP 힌트 (전문은 pull-on-demand).
|
|
375
|
+
* type/confidence 헤더는 의도적으로 생략 — 인덱스 라인은 포인터일 뿐, 상세는
|
|
376
|
+
* compound-read로 당겨 읽는다.
|
|
377
|
+
*/
|
|
378
|
+
export function buildIndexTier(sol, raw) {
|
|
379
|
+
let oneLiner = contentLines(raw)[0] ?? '';
|
|
380
|
+
if (oneLiner.length > INDEX_ONE_LINER_MAX_CHARS) {
|
|
381
|
+
oneLiner = `${oneLiner.slice(0, INDEX_ONE_LINER_MAX_CHARS - 3)}...`;
|
|
382
|
+
}
|
|
383
|
+
const hint = `compound-read("${sol.name}")`;
|
|
384
|
+
return oneLiner ? `${sol.name}: ${oneLiner} — ${hint}` : `${sol.name} — ${hint}`;
|
|
385
|
+
}
|
|
386
|
+
/** Tier 0 — 전문(Context+Content) 그대로. 파싱 실패/캡 초과면 null (호출부가 Tier 2로 폴백). */
|
|
387
|
+
export function buildFullTier(sol, raw) {
|
|
388
|
+
const parsed = parseSolutionV3(raw);
|
|
389
|
+
if (!parsed)
|
|
390
|
+
return null;
|
|
391
|
+
const body = [parsed.context, parsed.content].filter(Boolean).join('\n\n').trim();
|
|
392
|
+
if (!body || body.length > FULL_TEXT_MAX_CHARS)
|
|
393
|
+
return null;
|
|
394
|
+
const header = `${tierHeader(sol)}: ${sol.matchedTags.slice(0, 5).join(', ')}`;
|
|
395
|
+
return `${header}\n ${body.replace(/\n/g, '\n ')}`;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 매치 목록(relevance desc 정렬 가정)을 Progressive Disclosure 텍스트로 렌더한다.
|
|
399
|
+
* 순수 함수 — fs 읽기는 caller(main)가 `rawByName`으로 미리 채워 넣는다.
|
|
400
|
+
*
|
|
401
|
+
* - 매치가 정확히 1건 & 전문이 FULL_TEXT_MAX_CHARS 이하 → Tier 0(전문)
|
|
402
|
+
* - 그 외 상위 TOP_TIER_COUNT건 → Tier 2(요약)
|
|
403
|
+
* - 나머지 → Tier 1(인덱스 라인)
|
|
404
|
+
*/
|
|
405
|
+
export function renderSolutionTiers(sols, rawByName) {
|
|
406
|
+
const rendered = new Map();
|
|
407
|
+
const singleFullEligible = sols.length === 1;
|
|
408
|
+
sols.forEach((sol, rank) => {
|
|
409
|
+
const raw = rawByName.get(sol.name) ?? '';
|
|
410
|
+
if (singleFullEligible) {
|
|
411
|
+
rendered.set(sol.name, buildFullTier(sol, raw) ?? buildSummaryTier(sol, raw));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
rendered.set(sol.name, rank < TOP_TIER_COUNT ? buildSummaryTier(sol, raw) : buildIndexTier(sol, raw));
|
|
415
|
+
});
|
|
416
|
+
return rendered;
|
|
417
|
+
}
|
|
308
418
|
async function main() {
|
|
309
419
|
const _hookStart = Date.now();
|
|
310
420
|
try {
|
|
@@ -463,36 +573,21 @@ async function main() {
|
|
|
463
573
|
if (toInject.length >= Math.min(budget.solutionsPerPrompt, MAX_SOLUTIONS_PER_SESSION - injected.size))
|
|
464
574
|
break;
|
|
465
575
|
}
|
|
466
|
-
// Progressive Disclosure
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
const
|
|
470
|
-
const summaries = new Map();
|
|
471
|
-
const candidateEntries = [];
|
|
576
|
+
// Progressive Disclosure: 매치 랭크에 따라 Tier 0(전문)/2(요약)/1(인덱스)로 나눠 렌더.
|
|
577
|
+
// rank 순서는 toInject 자체(matchSolutions/applyRoiDemotions가 이미 relevance desc
|
|
578
|
+
// 정렬해 반환) — 여기서 별도 재정렬하지 않는다.
|
|
579
|
+
const rawByName = new Map();
|
|
472
580
|
for (const sol of toInject) {
|
|
473
|
-
let contentSnippet = '';
|
|
474
581
|
try {
|
|
475
|
-
|
|
476
|
-
const contentMatch = raw.match(/## Content\n([\s\S]*?)(?:\n## |\n---|$)/);
|
|
477
|
-
if (contentMatch) {
|
|
478
|
-
// 코드 블록 제거 후 핵심 텍스트만 추출, 최대 3줄
|
|
479
|
-
const lines = contentMatch[1]
|
|
480
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
481
|
-
.split('\n')
|
|
482
|
-
.map(l => l.trim())
|
|
483
|
-
.filter(l => l.length > 0);
|
|
484
|
-
contentSnippet = lines.slice(0, 3).join('\n');
|
|
485
|
-
if (contentSnippet.length > SUMMARY_MAX_CHARS) {
|
|
486
|
-
contentSnippet = `${contentSnippet.slice(0, SUMMARY_MAX_CHARS - 3)}...`;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
582
|
+
rawByName.set(sol.name, fs.readFileSync(sol.path, 'utf-8'));
|
|
489
583
|
}
|
|
490
|
-
catch { /* fail-open: 파일 읽기 실패 시
|
|
491
|
-
const header = `${sol.name} [${sol.type}|${sol.confidence.toFixed(2)}]: ${sol.matchedTags.slice(0, 5).join(', ')}`;
|
|
492
|
-
const summary = contentSnippet ? `${header}\n ${contentSnippet.replace(/\n/g, '\n ')}` : header;
|
|
493
|
-
summaries.set(sol.name, summary);
|
|
494
|
-
candidateEntries.push({ name: sol.name, chars: summary.length });
|
|
584
|
+
catch { /* fail-open: 파일 읽기 실패 시 이름+태그만으로 렌더 */ }
|
|
495
585
|
}
|
|
586
|
+
const summaries = renderSolutionTiers(toInject, rawByName);
|
|
587
|
+
const candidateEntries = toInject.map(sol => ({
|
|
588
|
+
name: sol.name,
|
|
589
|
+
chars: (summaries.get(sol.name) ?? sol.name).length,
|
|
590
|
+
}));
|
|
496
591
|
// H-1 + M-3 fix: lock 안 disjoint 검증으로 새로 추가된 entry만 반환받는다.
|
|
497
592
|
// 다른 hook이 같은 sessionId로 동시에 같은 솔루션을 inject했다면 이 hook의
|
|
498
593
|
// commit에서는 newlyAdded에 포함되지 않아 evidence 중복 카운트가 차단된다.
|
|
@@ -593,7 +688,10 @@ async function main() {
|
|
|
593
688
|
}).join('\n');
|
|
594
689
|
const header = `Matched solutions (apply these patterns to your response):\n`;
|
|
595
690
|
const footer = `\n\nAPPLY the patterns above to your response. If a pattern is directly relevant, follow its guidance. Use compound-read MCP tool for full details if needed.\nWhen using Grep or Bash, always set head_limit or pipe through | head -n to limit output size.`;
|
|
596
|
-
|
|
691
|
+
// 최종 안전판: 위 tier 배분이 어떤 조합이든 이 턴의 additionalContext는
|
|
692
|
+
// INJECTION_HARD_CAP_CHARS를 넘지 않는다 (context-budget.ts의 세션 누적
|
|
693
|
+
// 캡과는 별도 축 — 이건 "한 턴" 상한).
|
|
694
|
+
const fullInjection = truncateContent(header + injections + footer, INJECTION_HARD_CAP_CHARS);
|
|
597
695
|
// 플러그인 시그널 기록 (다른 플러그인이 참고할 수 있도록)
|
|
598
696
|
try {
|
|
599
697
|
writeSignal(sessionId, 'UserPromptSubmit', fullInjection.length);
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -25,6 +25,7 @@ import { readStdinJSON } from './shared/read-stdin.js';
|
|
|
25
25
|
import { approve, approveWithWarning, blockStop, failOpenWithTracking } from './shared/hook-response.js';
|
|
26
26
|
import { takeLastExtractionNotice } from '../core/extraction-notice.js';
|
|
27
27
|
import { runMetaGuards } from '../checks/_shared/meta-guard-dispatch.js';
|
|
28
|
+
import { guardModeForModel, readSessionModel } from '../checks/_shared/model-profile.js';
|
|
28
29
|
import { sanitizeForGuard } from '../checks/_shared/text-sanitizer.js';
|
|
29
30
|
import { STATE_DIR } from '../core/paths.js';
|
|
30
31
|
import { sanitizeId } from './shared/sanitize-id.js';
|
|
@@ -507,7 +508,9 @@ export async function main() {
|
|
|
507
508
|
if (process.env.FORGEN_USER_CONFIRMED !== '1') {
|
|
508
509
|
const sessionId = input?.session_id ?? 'unknown';
|
|
509
510
|
const recentTools = loadRecentToolNames(sessionId);
|
|
510
|
-
|
|
511
|
+
// W4-3: 세션 모델 기반 완료-가드 모드 (opus-4.8 실측 blocks=0 → advise)
|
|
512
|
+
const completionGuardMode = guardModeForModel(readSessionModel(sanitizeId(sessionId)));
|
|
513
|
+
const results = runMetaGuards({ lastMessage, recentTools, minMeasurements: 1, completionGuardMode });
|
|
511
514
|
for (const r of results) {
|
|
512
515
|
recordViolation({
|
|
513
516
|
rule_id: `builtin:${r.shortId}`,
|
|
@@ -564,7 +567,7 @@ export async function main() {
|
|
|
564
567
|
// pre-tool-use 와 일관된 FORGEN_USER_CONFIRMED=1 탈출구 + 영구 비활성화 CLI 노출.
|
|
565
568
|
const reasonWithHint = `[forgen:stop-guard/${hit.id.slice(0, 8)}] ${reason}
|
|
566
569
|
|
|
567
|
-
(Override this turn: set FORGEN_USER_CONFIRMED=1 (audited). Disable rule permanently: \`forgen suppress-rule ${hit.id}\`. See recent blocks: \`forgen
|
|
570
|
+
(Override this turn: set FORGEN_USER_CONFIRMED=1 (audited). Disable rule permanently: \`forgen suppress-rule ${hit.id}\`. See recent blocks: \`forgen status --blocks\`.)`;
|
|
568
571
|
const count = incrementBlockCount(sessionId, hit.id);
|
|
569
572
|
const threshold = getStuckLoopThreshold();
|
|
570
573
|
if (count > threshold) {
|
|
@@ -27,6 +27,7 @@ import { sanitizeId } from './shared/sanitize-id.js';
|
|
|
27
27
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
28
28
|
import { STATE_DIR } from '../core/paths.js';
|
|
29
29
|
import { runMetaGuards } from '../checks/_shared/meta-guard-dispatch.js';
|
|
30
|
+
import { guardModeForModel, readSessionModel } from '../checks/_shared/model-profile.js';
|
|
30
31
|
import { recordViolation } from '../engine/lifecycle/signals.js';
|
|
31
32
|
import { incrementBlockCount, resetBlockCount, getStuckLoopThreshold, logDriftEvent, } from './stop-guard.js';
|
|
32
33
|
const HOOK_NAME = 'subagent-stop-guard';
|
|
@@ -108,7 +109,9 @@ export async function main() {
|
|
|
108
109
|
const sessionId = input?.session_id ?? 'unknown';
|
|
109
110
|
const agentId = input?.agent_id ?? input?.agentId ?? 'unknown';
|
|
110
111
|
const recentTools = loadAgentRecentTools(sessionId, agentId);
|
|
111
|
-
|
|
112
|
+
// W4-3: 세션 모델 기반 완료-가드 모드 (opus-4.8 실측 blocks=0 → advise)
|
|
113
|
+
const completionGuardMode = guardModeForModel(readSessionModel(sanitizeId(sessionId)));
|
|
114
|
+
const results = runMetaGuards({ lastMessage, recentTools, minMeasurements: 1, completionGuardMode });
|
|
112
115
|
// 2c: stuck-loop 카운터를 (sessionId, agentId) 로 분리해 동시 subagent 간 충돌 방지.
|
|
113
116
|
const counterKey = `${sessionId}:${agentId}`;
|
|
114
117
|
for (const r of results) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode HostCapabilities — Multi-Harness Adapter Plan P1
|
|
3
|
+
* (`reports/harness-probe/adapter-plan-2026-07-20.md` §2.2 / §3.1).
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ 검증 수준(정직): verificationLevel='docs' 유지. OpenCode plugin 문서 + context7
|
|
6
|
+
* 실소스 대조(`/anomalyco/opencode`: tool.execute.before(input.tool, output.args), throw=block
|
|
7
|
+
* 확인). codex 의 로컬-schema source-level 과는 다른 축.
|
|
8
|
+
*
|
|
9
|
+
* 2026-07-22 **런타임 부분 검증(opencode v1.1.8, 격리 XDG)**: `forgen install opencode` 배포 후
|
|
10
|
+
* - plugin(`plugins/forgen.ts`) **로드 성공**(service=plugin loading plugin, 에러 없음),
|
|
11
|
+
* - MCP(`mcp.forgen-compound`) **등록 + 9 tools** 로드 성공(toolCount=9).
|
|
12
|
+
* 미검증: tool.execute.before 발화(guard block) end-to-end 는 model API 필요(격리 세션에서
|
|
13
|
+
* 401 Missing API key 로 model 단계 미도달) → verificationLevel='docs' 유지가 정직.
|
|
14
|
+
* plugin-load + MCP 는 runtime-confirmed, guard-firing 은 model-backed 세션 테스트 대기.
|
|
15
|
+
*
|
|
16
|
+
* OpenCode 통합 형태는 Claude/Codex 와 다르다: subprocess-stdin hook 이 아니라
|
|
17
|
+
* **in-process TypeScript plugin**(`.opencode/plugins/`, 25+ 이벤트, tool.execute.before
|
|
18
|
+
* throw-to-block). 이 형태 차이 때문에 P1 은 plugin 슬림이 최대 신규 델타(plan §5 blocker 1).
|
|
19
|
+
*
|
|
20
|
+
* 근거 소스:
|
|
21
|
+
* - OpenCode plugins docs: https://opencode.ai/docs/plugins/
|
|
22
|
+
* - plan §2.2 (tool.execute.before throw, session.idle, experimental.session.compacting,
|
|
23
|
+
* tui.prompt.append, opencode.jsonc MCP, message.updated/session.* event stream)
|
|
24
|
+
*/
|
|
25
|
+
import type { HostCapabilities } from '../core/trust-layer-intent.js';
|
|
26
|
+
export declare const opencodeCapabilities: HostCapabilities;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode HostCapabilities — Multi-Harness Adapter Plan P1
|
|
3
|
+
* (`reports/harness-probe/adapter-plan-2026-07-20.md` §2.2 / §3.1).
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ 검증 수준(정직): verificationLevel='docs' 유지. OpenCode plugin 문서 + context7
|
|
6
|
+
* 실소스 대조(`/anomalyco/opencode`: tool.execute.before(input.tool, output.args), throw=block
|
|
7
|
+
* 확인). codex 의 로컬-schema source-level 과는 다른 축.
|
|
8
|
+
*
|
|
9
|
+
* 2026-07-22 **런타임 부분 검증(opencode v1.1.8, 격리 XDG)**: `forgen install opencode` 배포 후
|
|
10
|
+
* - plugin(`plugins/forgen.ts`) **로드 성공**(service=plugin loading plugin, 에러 없음),
|
|
11
|
+
* - MCP(`mcp.forgen-compound`) **등록 + 9 tools** 로드 성공(toolCount=9).
|
|
12
|
+
* 미검증: tool.execute.before 발화(guard block) end-to-end 는 model API 필요(격리 세션에서
|
|
13
|
+
* 401 Missing API key 로 model 단계 미도달) → verificationLevel='docs' 유지가 정직.
|
|
14
|
+
* plugin-load + MCP 는 runtime-confirmed, guard-firing 은 model-backed 세션 테스트 대기.
|
|
15
|
+
*
|
|
16
|
+
* OpenCode 통합 형태는 Claude/Codex 와 다르다: subprocess-stdin hook 이 아니라
|
|
17
|
+
* **in-process TypeScript plugin**(`.opencode/plugins/`, 25+ 이벤트, tool.execute.before
|
|
18
|
+
* throw-to-block). 이 형태 차이 때문에 P1 은 plugin 슬림이 최대 신규 델타(plan §5 blocker 1).
|
|
19
|
+
*
|
|
20
|
+
* 근거 소스:
|
|
21
|
+
* - OpenCode plugins docs: https://opencode.ai/docs/plugins/
|
|
22
|
+
* - plan §2.2 (tool.execute.before throw, session.idle, experimental.session.compacting,
|
|
23
|
+
* tui.prompt.append, opencode.jsonc MCP, message.updated/session.* event stream)
|
|
24
|
+
*/
|
|
25
|
+
export const opencodeCapabilities = {
|
|
26
|
+
hostId: 'opencode',
|
|
27
|
+
// docs-level: OpenCode 문서 기반 선언, forgen 배선 미완(plugin 슬림 전). 아래 status 의
|
|
28
|
+
// supported 는 "플랫폼 가능"이지 "forgen 강제"가 아니다 — intentEnforced 로 게이트할 것.
|
|
29
|
+
verificationLevel: 'docs',
|
|
30
|
+
intents: {
|
|
31
|
+
'block-completion': {
|
|
32
|
+
// session.idle 는 발화하나 "완료 강제 차단/재-turn" 계약이 문서에 없다.
|
|
33
|
+
status: 'unsupported',
|
|
34
|
+
expression: 'session.idle 이벤트는 관측 가능하나 force-continue 표면 부재 → advise-only',
|
|
35
|
+
mitigation: 'ADR-010 F1(frontier blocks=0)과 동일한 advise-mode: tui.toast.show 로 사용자에게 표면화하되 하드 차단은 주장하지 않는다. (model,harness)→policy 축으로 advise 처리.',
|
|
36
|
+
source: 'plan §2.2/§3.1 — OpenCode docs 에 완료 방지 API 미문서화. Cursor 와 동일 한계.',
|
|
37
|
+
},
|
|
38
|
+
'block-tool-use': {
|
|
39
|
+
// tool.execute.before 에서 throw 하면 *모든* tool 차단 (Cursor 의 shell/MCP/read 한정보다 강함).
|
|
40
|
+
status: 'supported',
|
|
41
|
+
expression: 'plugin `tool.execute.before` 훅에서 throw → 해당 tool 실행 차단(전 tool 대상)',
|
|
42
|
+
source: 'https://opencode.ai/docs/plugins/ — docs 예시가 .env read 를 tool.execute.before throw 로 차단. plan §2.2.',
|
|
43
|
+
},
|
|
44
|
+
'inject-context': {
|
|
45
|
+
// experimental.session.compacting(output.context/prompt) + tui.prompt.append/session.created.
|
|
46
|
+
status: 'partial',
|
|
47
|
+
expression: 'experimental.session.compacting 로 output.context/prompt 수정 + tui.prompt.append/session.created 로 주입',
|
|
48
|
+
mitigation: 'experimental 표면이라 안정성 미보장 — 안정 계약 확정 전까지 정적 룰(AGENTS.md/.opencode) 폴백을 1차로 유지하고 동적 주입은 best-effort.',
|
|
49
|
+
source: 'plan §2.2 — experimental.session.compacting / tui.prompt.append (less-stable surface).',
|
|
50
|
+
},
|
|
51
|
+
'observe-only': {
|
|
52
|
+
// 이벤트 스트림(message.updated/session.*) → observer log. host 무관 stdout.
|
|
53
|
+
status: 'supported',
|
|
54
|
+
expression: 'message.updated/session.* 이벤트 스트림 → observer log (denyOrObserve 는 host 무관 stdout)',
|
|
55
|
+
source: 'plan §2.2 event stream; forgen denyOrObserve 는 stdout JSON 만 다뤄 host 무관.',
|
|
56
|
+
},
|
|
57
|
+
'secret-filter': {
|
|
58
|
+
// tool.execute.before(pre-block) + tool.execute.after(post-redact). pre 가드가 1차.
|
|
59
|
+
status: 'supported',
|
|
60
|
+
expression: 'tool.execute.before 로 자격증명 tool 입력 차단(pre) + tool.execute.after 로 결과 redact(post)',
|
|
61
|
+
mitigation: 'Claude 와 동일하게 PreToolUse 등가(tool.execute.before) 가드가 1차 방어. post-redact 는 after 훅으로 보강.',
|
|
62
|
+
source: 'plan §3.1 — OpenCode 는 pre+post 둘 다 표면 보유(Codex 의 post-only partial 보다 강함).',
|
|
63
|
+
},
|
|
64
|
+
'forge-loop-state-inject': {
|
|
65
|
+
// inject-context 와 동일 표면(experimental) — ≤1KB forge-loop-state.
|
|
66
|
+
status: 'partial',
|
|
67
|
+
expression: '<forge-loop-state> ≤1KB 를 tui.prompt.append/session.created 로 주입(experimental 표면 공유)',
|
|
68
|
+
mitigation: 'inject-context 와 동일한 experimental 한계 — 안정 계약 전까지 best-effort.',
|
|
69
|
+
source: 'plan §2.2/§3.1 — inject-context 와 같은 표면 재사용.',
|
|
70
|
+
},
|
|
71
|
+
'self-evidence-record': {
|
|
72
|
+
// 이벤트 스트림 캡처 → ~/.forgen/state/*.json, host:"opencode" 태그.
|
|
73
|
+
status: 'supported',
|
|
74
|
+
expression: 'session.*/message.updated 캡처 → ~/.forgen/state/*.json (host 무관). evidence 에 host:"opencode" 태그.',
|
|
75
|
+
source: 'plan §2.2 event stream — file-transcript polling 보다 richer. spec §4.2 host-tagged evidence.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -9,3 +9,10 @@ import { type HostCapabilities, type HostId, type TrustLayerIntent } from '../co
|
|
|
9
9
|
export declare function getHostCapabilities(host: HostId): HostCapabilities;
|
|
10
10
|
export declare function listRegisteredHosts(): readonly HostId[];
|
|
11
11
|
export declare function intentSupported(host: HostId, intent: TrustLayerIntent): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* "forgen 이 이 host 에서 이 의도를 *실제로 강제/실행* 하는가" (W3-3 리뷰 SEV-3 #1).
|
|
14
|
+
* intentSupported 와 달리 verificationLevel 을 게이트한다 — 'docs'(플랫폼-가능하나 미배선,
|
|
15
|
+
* 예: opencode P1)는 supported 여도 false. 프로그램 소비자가 "선언"과 "실배선"을 혼동하지
|
|
16
|
+
* 않도록 하는 정직 게이트.
|
|
17
|
+
*/
|
|
18
|
+
export declare function intentEnforced(host: HostId, intent: TrustLayerIntent): boolean;
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
import { assertCapabilitiesComplete, } from '../core/trust-layer-intent.js';
|
|
9
9
|
import { claudeCapabilities } from './capabilities-claude.js';
|
|
10
10
|
import { codexCapabilities } from './capabilities-codex.js';
|
|
11
|
+
import { opencodeCapabilities } from './capabilities-opencode.js';
|
|
11
12
|
const REGISTRY = new Map([
|
|
12
13
|
[claudeCapabilities.hostId, claudeCapabilities],
|
|
13
14
|
[codexCapabilities.hostId, codexCapabilities],
|
|
15
|
+
[opencodeCapabilities.hostId, opencodeCapabilities],
|
|
14
16
|
]);
|
|
15
17
|
// 모듈 로드 시점 자기 검증 — 하나라도 미선언이면 즉시 throw.
|
|
16
18
|
for (const caps of REGISTRY.values()) {
|
|
@@ -28,3 +30,15 @@ export function listRegisteredHosts() {
|
|
|
28
30
|
export function intentSupported(host, intent) {
|
|
29
31
|
return getHostCapabilities(host).intents[intent].status === 'supported';
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* "forgen 이 이 host 에서 이 의도를 *실제로 강제/실행* 하는가" (W3-3 리뷰 SEV-3 #1).
|
|
35
|
+
* intentSupported 와 달리 verificationLevel 을 게이트한다 — 'docs'(플랫폼-가능하나 미배선,
|
|
36
|
+
* 예: opencode P1)는 supported 여도 false. 프로그램 소비자가 "선언"과 "실배선"을 혼동하지
|
|
37
|
+
* 않도록 하는 정직 게이트.
|
|
38
|
+
*/
|
|
39
|
+
export function intentEnforced(host, intent) {
|
|
40
|
+
const caps = getHostCapabilities(host);
|
|
41
|
+
if (caps.verificationLevel === 'docs')
|
|
42
|
+
return false;
|
|
43
|
+
return caps.intents[intent].status === 'supported';
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex 개인화 룰 주입 — ADR-014 D1
|
|
3
|
+
*
|
|
4
|
+
* Claude 는 `.claude/rules/*.md` 파일을 Claude Code 가 매 턴 로드하지만 Codex 에는 그 표면이 없다.
|
|
5
|
+
* 본 모듈은 *같은 소스* (generateClaudeRuleFiles) 를 *같은 캡* (RULE_FILE_CAPS) 으로 렌더해
|
|
6
|
+
* SessionStart additionalContext 로 넣을 블록을 만든다. hooks.json 을 바꾸지 않기 위해 새 훅이
|
|
7
|
+
* 아니라 session-recovery 내부의 codex 분기에서 호출된다.
|
|
8
|
+
*
|
|
9
|
+
* 컴팩션 후 재주입은 별도 경로가 없다: Claude Code 와 Codex 모두 compaction 시 SessionStart 를
|
|
10
|
+
* source="compact" 로 다시 발화하므로 (Codex 공식 hooks 문서), 같은 경로가 한 번 더 돈다.
|
|
11
|
+
* (critic 리뷰 2026-10-01: PreCompact 플래그 경로는 2중 주입이라 제거.)
|
|
12
|
+
*/
|
|
13
|
+
export declare const FORGEN_RULES_TAG = "forgen-rules";
|
|
14
|
+
/** codex-adapter 가 delegate hook 에 FORGEN_RUNTIME=codex 를 주입한다 (0.4.6+). */
|
|
15
|
+
export declare function isCodexRuntime(env?: NodeJS.ProcessEnv): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* 룰 파일 맵 → 단일 additionalContext 블록. Claude 의 injectClaudeRuleFiles 와 동일 캡:
|
|
18
|
+
* 파일당 perRuleFile, 총량 totalRuleFiles. 캡 초과 파일은 잘리고, 총량 초과 시 이후 파일은 생략.
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderCodexRulesBlock(ruleFiles: Record<string, string>): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* cwd + 렌더된 v1 룰 → Codex 주입 블록. config-injector 는 무거워서 lazy import.
|
|
23
|
+
* 실패 시 null (fail-open — 룰 주입 실패가 세션 시작을 막으면 안 된다).
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildCodexRulesContext(cwd: string, renderedRules: string | null): Promise<string | null>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex 개인화 룰 주입 — ADR-014 D1
|
|
3
|
+
*
|
|
4
|
+
* Claude 는 `.claude/rules/*.md` 파일을 Claude Code 가 매 턴 로드하지만 Codex 에는 그 표면이 없다.
|
|
5
|
+
* 본 모듈은 *같은 소스* (generateClaudeRuleFiles) 를 *같은 캡* (RULE_FILE_CAPS) 으로 렌더해
|
|
6
|
+
* SessionStart additionalContext 로 넣을 블록을 만든다. hooks.json 을 바꾸지 않기 위해 새 훅이
|
|
7
|
+
* 아니라 session-recovery 내부의 codex 분기에서 호출된다.
|
|
8
|
+
*
|
|
9
|
+
* 컴팩션 후 재주입은 별도 경로가 없다: Claude Code 와 Codex 모두 compaction 시 SessionStart 를
|
|
10
|
+
* source="compact" 로 다시 발화하므로 (Codex 공식 hooks 문서), 같은 경로가 한 번 더 돈다.
|
|
11
|
+
* (critic 리뷰 2026-10-01: PreCompact 플래그 경로는 2중 주입이라 제거.)
|
|
12
|
+
*/
|
|
13
|
+
import { RULE_FILE_CAPS } from '../hooks/shared/injection-caps.js';
|
|
14
|
+
export const FORGEN_RULES_TAG = 'forgen-rules';
|
|
15
|
+
/** codex-adapter 가 delegate hook 에 FORGEN_RUNTIME=codex 를 주입한다 (0.4.6+). */
|
|
16
|
+
export function isCodexRuntime(env = process.env) {
|
|
17
|
+
return env.FORGEN_RUNTIME === 'codex';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 룰 파일 맵 → 단일 additionalContext 블록. Claude 의 injectClaudeRuleFiles 와 동일 캡:
|
|
21
|
+
* 파일당 perRuleFile, 총량 totalRuleFiles. 캡 초과 파일은 잘리고, 총량 초과 시 이후 파일은 생략.
|
|
22
|
+
*/
|
|
23
|
+
export function renderCodexRulesBlock(ruleFiles) {
|
|
24
|
+
const entries = Object.entries(ruleFiles).filter(([, c]) => typeof c === 'string' && c.trim().length > 0);
|
|
25
|
+
if (entries.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
const PER = RULE_FILE_CAPS.perRuleFile;
|
|
28
|
+
const TOTAL = RULE_FILE_CAPS.totalRuleFiles;
|
|
29
|
+
const sections = [];
|
|
30
|
+
let total = 0;
|
|
31
|
+
for (const [filename, content] of entries) {
|
|
32
|
+
const capped = content.length > PER
|
|
33
|
+
? `${content.slice(0, PER)}\n... (capped at rule file limit)\n`
|
|
34
|
+
: content;
|
|
35
|
+
if (total + capped.length > TOTAL)
|
|
36
|
+
break;
|
|
37
|
+
total += capped.length;
|
|
38
|
+
sections.push(`<!-- ${filename} -->\n${capped.trim()}`);
|
|
39
|
+
}
|
|
40
|
+
if (sections.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return [
|
|
43
|
+
`<${FORGEN_RULES_TAG} host="codex">`,
|
|
44
|
+
'These are the same forgen rules Claude Code loads from .claude/rules/. They apply to this Codex session. Follow them.',
|
|
45
|
+
'',
|
|
46
|
+
sections.join('\n\n---\n\n'),
|
|
47
|
+
`</${FORGEN_RULES_TAG}>`,
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* cwd + 렌더된 v1 룰 → Codex 주입 블록. config-injector 는 무거워서 lazy import.
|
|
52
|
+
* 실패 시 null (fail-open — 룰 주입 실패가 세션 시작을 막으면 안 된다).
|
|
53
|
+
*/
|
|
54
|
+
export async function buildCodexRulesContext(cwd, renderedRules) {
|
|
55
|
+
try {
|
|
56
|
+
const { generateClaudeRuleFiles } = await import('../core/config-injector.js');
|
|
57
|
+
return renderCodexRulesBlock(generateClaudeRuleFiles(cwd, renderedRules));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|