@wooojin/forgen 0.4.13 → 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 +94 -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 +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 +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
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;
|
|
@@ -9,7 +9,8 @@ import * as path from 'node:path';
|
|
|
9
9
|
import * as crypto from 'node:crypto';
|
|
10
10
|
import { ME_BEHAVIOR } 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 } 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';
|
|
@@ -32,11 +33,13 @@ function evidencePath(evidenceId) {
|
|
|
32
33
|
function detectHost(explicit) {
|
|
33
34
|
if (explicit)
|
|
34
35
|
return explicit;
|
|
36
|
+
// W3-3 리뷰 SEV-3 #5: 이진비교(=== 'claude' || === 'codex') 대신 HOST_IDS 정준목록으로
|
|
37
|
+
// 유효 host 를 해소해 FORGEN_HOST/FORGEN_RUNTIME='opencode' 도 보존(self-evidence-record 일관).
|
|
35
38
|
const fromEnv = process.env.FORGEN_HOST;
|
|
36
|
-
if (fromEnv
|
|
39
|
+
if (fromEnv && HOST_IDS.includes(fromEnv))
|
|
37
40
|
return fromEnv;
|
|
38
41
|
const fromRuntime = process.env.FORGEN_RUNTIME;
|
|
39
|
-
if (fromRuntime
|
|
42
|
+
if (fromRuntime && HOST_IDS.includes(fromRuntime))
|
|
40
43
|
return fromRuntime;
|
|
41
44
|
if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX_NETWORK_DISABLED)
|
|
42
45
|
return 'codex';
|
|
@@ -111,13 +114,17 @@ export function appendEvidence(evidence) {
|
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
/**
|
|
114
|
-
* 기존 evidence 에 host 필드가
|
|
115
|
-
* 새 multi-host 도입 이전 데이터는 모두 Claude
|
|
117
|
+
* 기존 evidence 에 host 필드가 없거나 유효하지 않으면 'claude' 로 backfill
|
|
118
|
+
* (Multi-Host §4.2 마이그레이션 정책). 새 multi-host 도입 이전 데이터는 모두 Claude 발생.
|
|
119
|
+
*
|
|
120
|
+
* W3-3 리뷰 SEV-3 #5: 이전엔 `!== 'claude' && !== 'codex'` 라 **유효한 'opencode' 태그도
|
|
121
|
+
* claude 로 clobber** → self-evidence-record='supported'(host:"opencode") 선언과 정면 모순.
|
|
122
|
+
* 이제 HOST_IDS 정준 목록으로 "유효 host" 를 판정해 opencode 등 모든 유효 태그를 보존한다.
|
|
116
123
|
*/
|
|
117
124
|
function backfillHost(ev) {
|
|
118
125
|
if (!ev)
|
|
119
126
|
return ev;
|
|
120
|
-
if (ev.host
|
|
127
|
+
if (ev.host && HOST_IDS.includes(ev.host))
|
|
121
128
|
return ev;
|
|
122
129
|
return { ...ev, host: 'claude' };
|
|
123
130
|
}
|
|
@@ -163,7 +170,41 @@ export function loadPromotionCandidates() {
|
|
|
163
170
|
* 동일 render_key를 가진 scope:'me' 규칙이 이미 있으면 건너뜀.
|
|
164
171
|
* @returns 승격된 규칙 수
|
|
165
172
|
*/
|
|
173
|
+
/**
|
|
174
|
+
* ADR-013 채굴 교정 안전 파라미터 (리뷰 SEV-1 대응):
|
|
175
|
+
* - 채굴 룰은 "auto:" render_key 네임스페이스로 분리 → 실시간 명시 교정과 절대 충돌/억제 안 함.
|
|
176
|
+
* - advisory-only: enforce_via=[] (차단 메커니즘 미부여). 환각 교정이 차단 룰이 되는 것 근절.
|
|
177
|
+
* - 생성나이 은퇴: TTL 지난 미확인 채굴 룰 retire (me-scope 룰이 매 세션 주입돼 ROI/T4 로는
|
|
178
|
+
* 은퇴 안 되던 문제의 실제 해소). + 라이브 채굴 룰 총량 상한.
|
|
179
|
+
*/
|
|
180
|
+
const AUTO_MINED_PREFIX = 'auto:';
|
|
181
|
+
const AUTO_MINED_TTL_MS = 21 * 24 * 60 * 60 * 1000; // 21일
|
|
182
|
+
const AUTO_MINED_MAX_LIVE = 30;
|
|
183
|
+
/** 채굴(behavior_inference) 룰 은퇴: TTL 초과 + 총량 상한 초과분(오래된 순). */
|
|
184
|
+
function retireStaleAutoMinedRules(now) {
|
|
185
|
+
const auto = loadActiveRules().filter((r) => r.scope === 'me' && r.source === 'behavior_inference' && r.render_key?.startsWith(AUTO_MINED_PREFIX));
|
|
186
|
+
const byAge = auto
|
|
187
|
+
.map((r) => ({ r, age: now - Date.parse(r.created_at) }))
|
|
188
|
+
.sort((a, b) => b.age - a.age); // 오래된 순
|
|
189
|
+
const keep = [];
|
|
190
|
+
for (const item of byAge) {
|
|
191
|
+
if (item.age >= AUTO_MINED_TTL_MS) {
|
|
192
|
+
updateRuleStatus(item.r.rule_id, 'removed'); // TTL 초과 → 은퇴
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
keep.push(item);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 총량 상한 초과분(가장 오래된 것부터) 은퇴.
|
|
199
|
+
const excess = keep.length - AUTO_MINED_MAX_LIVE;
|
|
200
|
+
for (let i = 0; i < excess; i++)
|
|
201
|
+
updateRuleStatus(keep[i].r.rule_id, 'removed');
|
|
202
|
+
}
|
|
166
203
|
export function promoteSessionCandidates(sessionId) {
|
|
204
|
+
// 채굴 룰 TTL 은퇴는 **early-return 앞에서** 실행 (critic 재검증 #1): 승급 후보가 없는
|
|
205
|
+
// 조용한 세션에서도 wall-clock TTL 이 실제로 강제되도록. (auto-compound-runner 는 매
|
|
206
|
+
// 세션 종료 시 promoteSessionCandidates 를 호출하므로 이 경로가 세션당 1회 보장된다.)
|
|
207
|
+
retireStaleAutoMinedRules(Date.now());
|
|
167
208
|
const candidates = loadPromotionCandidates().filter(e => e.session_id === sessionId);
|
|
168
209
|
if (candidates.length === 0)
|
|
169
210
|
return 0;
|
|
@@ -175,30 +216,43 @@ export function promoteSessionCandidates(sessionId) {
|
|
|
175
216
|
const axisHint = payload?.axis_hint;
|
|
176
217
|
const target = payload?.target;
|
|
177
218
|
const kind = payload?.kind;
|
|
219
|
+
const autoMined = payload?.auto_mined === true;
|
|
178
220
|
if (!target)
|
|
179
221
|
continue;
|
|
180
|
-
|
|
222
|
+
// 채굴 룰은 "auto:" 네임스페이스 — 실시간 교정 render_key 와 절대 충돌 안 함 (SEV-2 #4).
|
|
223
|
+
const baseKey = `${axisHint ?? 'workflow'}.${target.toLowerCase().replace(/\s+/g, '-').slice(0, 30)}`;
|
|
224
|
+
const renderKey = autoMined ? `${AUTO_MINED_PREFIX}${baseKey}` : baseKey;
|
|
181
225
|
if (existingRenderKeys.has(renderKey))
|
|
182
226
|
continue;
|
|
183
227
|
const category = axisHint === 'quality_safety' ? 'quality'
|
|
184
228
|
: axisHint === 'autonomy' ? 'autonomy'
|
|
185
229
|
: 'workflow';
|
|
230
|
+
// ADR-013: 채굴 교정(auto_mined)은 실시간 명시 교정과 엄격 차등 — provenance
|
|
231
|
+
// =behavior_inference, strength 절대 strong 아님(default).
|
|
186
232
|
let rule = createRule({
|
|
187
233
|
category,
|
|
188
234
|
scope: 'me',
|
|
189
235
|
trigger: target,
|
|
190
236
|
policy: candidate.summary,
|
|
191
|
-
strength: kind === 'avoid-this' ? 'strong' : 'default',
|
|
192
|
-
source: 'explicit_correction',
|
|
237
|
+
strength: autoMined ? 'default' : kind === 'avoid-this' ? 'strong' : 'default',
|
|
238
|
+
source: autoMined ? 'behavior_inference' : 'explicit_correction',
|
|
193
239
|
evidence_refs: [candidate.evidence_id],
|
|
194
240
|
render_key: renderKey,
|
|
195
241
|
});
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
242
|
+
if (autoMined) {
|
|
243
|
+
// advisory-only (SEV-1 #2): classify() 를 돌리지 않고 enforce_via 를 비워, 채굴 룰이
|
|
244
|
+
// 절대 Mech-A 차단(PreToolUse/Stop block)을 얻지 못하게 한다. 컨텍스트 주입만 되고
|
|
245
|
+
// 어떤 훅도 강제하지 않음 — 환각 교정이 영구 차단 룰이 되는 위험 근절.
|
|
246
|
+
rule.enforce_via = [];
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// ADR-001 auto-classify — 실시간 승격 rule 에만 enforce_via 자동 주입.
|
|
250
|
+
try {
|
|
251
|
+
const proposal = classify(rule);
|
|
252
|
+
rule = applyProposal(rule, proposal);
|
|
253
|
+
}
|
|
254
|
+
catch { /* fail-open */ }
|
|
200
255
|
}
|
|
201
|
-
catch { /* fail-open */ }
|
|
202
256
|
saveRule(rule);
|
|
203
257
|
existingRenderKeys.add(renderKey);
|
|
204
258
|
promoted++;
|
|
@@ -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
|
/** 테스트 노출용 — 임계값이 변경되면 회귀 즉시 감지. */
|
|
@@ -15,19 +15,19 @@ import { loadAllEvidence } from './evidence-store.js';
|
|
|
15
15
|
const DOMINANCE_THRESHOLD = 0.8; // 80% 이상이 한 host 에서 발생하면 dominant.
|
|
16
16
|
const MIN_TOTAL_FOR_DEMOTE = 5; // 너무 적은 표본은 무시.
|
|
17
17
|
function summarize(events) {
|
|
18
|
-
const byHost = { claude: 0, codex: 0 };
|
|
18
|
+
const byHost = { claude: 0, codex: 0, opencode: 0 };
|
|
19
19
|
for (const e of events) {
|
|
20
20
|
const h = (e.host ?? 'claude');
|
|
21
21
|
byHost[h] += 1;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
// host-agnostic 집계 — 새 host 추가 시 이 로직은 수정 불필요.
|
|
24
|
+
const entries = Object.entries(byHost);
|
|
25
|
+
const total = entries.reduce((s, [, n]) => s + n, 0);
|
|
24
26
|
if (total === 0) {
|
|
25
27
|
return { byHost, total, skew: 0, dominantHost: null, demoteRecommended: false };
|
|
26
28
|
}
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const dominant = claudeShare >= codexShare ? 'claude' : 'codex';
|
|
30
|
-
const skew = Math.max(claudeShare, codexShare);
|
|
29
|
+
const [dominant, dominantCount] = entries.reduce((max, cur) => (cur[1] > max[1] ? cur : max));
|
|
30
|
+
const skew = dominantCount / total;
|
|
31
31
|
const demoteRecommended = total >= MIN_TOTAL_FOR_DEMOTE && skew >= DOMINANCE_THRESHOLD;
|
|
32
32
|
return { byHost, total, skew, dominantHost: dominant, demoteRecommended };
|
|
33
33
|
}
|
|
@@ -53,10 +53,10 @@ export function summarizeNegativeSignalsForRef(refId) {
|
|
|
53
53
|
}
|
|
54
54
|
export function summarizeAllByHost() {
|
|
55
55
|
const all = loadAllEvidence();
|
|
56
|
-
const r = { claude: 0, codex: 0 };
|
|
56
|
+
const r = { claude: 0, codex: 0, opencode: 0 };
|
|
57
57
|
for (const e of all)
|
|
58
58
|
r[(e.host ?? 'claude')] += 1;
|
|
59
|
-
return { ...r, total: r.claude + r.codex };
|
|
59
|
+
return { ...r, total: r.claude + r.codex + r.opencode };
|
|
60
60
|
}
|
|
61
61
|
/** 테스트 노출용 — 임계값이 변경되면 회귀 즉시 감지. */
|
|
62
62
|
export const HOST_MISMATCH_TUNING = {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Authoritative schema: docs/plans/2026-04-03-forgen-data-model-storage-spec.md §2
|
|
6
6
|
*/
|
|
7
7
|
import type { Profile, QualityPack, AutonomyPack, JudgmentPack, CommunicationPack, TrustPolicy } from './types.js';
|
|
8
|
+
import type { HostId } from '../core/trust-layer-intent.js';
|
|
8
9
|
export declare function createProfile(userId: string, qualityPack: QualityPack, autonomyPack: AutonomyPack, trustPolicy: TrustPolicy, trustSource: Profile['trust_preferences']['source'], judgmentPack?: JudgmentPack, communicationPack?: CommunicationPack): Profile;
|
|
9
10
|
export declare function loadProfile(): Profile | null;
|
|
10
11
|
export declare function loadProfileRaw(): unknown;
|
|
@@ -50,7 +51,7 @@ export declare function bumpAxisConfidence(axis: 'quality_safety' | 'autonomy' |
|
|
|
50
51
|
* fgx / forgen 무인자 실행 시 어느 host 를 spawn 할지 결정. 'ask' 면 매번 묻기.
|
|
51
52
|
* 미설정(undefined) 은 legacy 사용자 호환 — 'claude' 로 resolve.
|
|
52
53
|
*/
|
|
53
|
-
export type DefaultHost =
|
|
54
|
+
export type DefaultHost = HostId | 'ask';
|
|
54
55
|
export declare function getDefaultHost(): DefaultHost | undefined;
|
|
55
56
|
export declare function setDefaultHost(host: DefaultHost): boolean;
|
|
56
57
|
/**
|
|
@@ -58,4 +59,4 @@ export declare function setDefaultHost(host: DefaultHost): boolean;
|
|
|
58
59
|
* 우선순위: explicit override > profile.default_host > 'claude' fallback.
|
|
59
60
|
* 'ask' 는 caller 가 별도 처리 (interactive prompt).
|
|
60
61
|
*/
|
|
61
|
-
export declare function resolveDefaultHost(override?:
|
|
62
|
+
export declare function resolveDefaultHost(override?: HostId): DefaultHost;
|
package/dist/store/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Authoritative source: docs/plans/2026-04-03-forgen-data-model-storage-spec.md
|
|
5
5
|
* Runtime contracts: docs/plans/2026-04-03-forgen-component-interface-design.md
|
|
6
6
|
*/
|
|
7
|
+
import type { HostId } from '../core/trust-layer-intent.js';
|
|
7
8
|
export type QualityPack = '보수형' | '균형형' | '속도형';
|
|
8
9
|
export type AutonomyPack = '확인 우선형' | '균형형' | '자율 실행형';
|
|
9
10
|
export type JudgmentPack = '최소변경형' | '균형형' | '구조적접근형';
|
|
@@ -110,6 +111,12 @@ export interface Rule {
|
|
|
110
111
|
* ADR-002 §Data Model.
|
|
111
112
|
*/
|
|
112
113
|
lifecycle?: LifecycleState;
|
|
114
|
+
/**
|
|
115
|
+
* W3-2 (교정 클러스터링): 이 rule 이 클러스터 통합으로 superseded 되었다면, 통합 결과
|
|
116
|
+
* rule 의 rule_id. `forgen rules --unmerge-cluster` 로 복원 시 이 링크로 원본을 되살린다.
|
|
117
|
+
* 통합 rule 쪽에는 설정되지 않는다(원본에만). ADR-010 W3-2.
|
|
118
|
+
*/
|
|
119
|
+
clustered_into?: string;
|
|
113
120
|
}
|
|
114
121
|
export type EvidenceType = 'explicit_correction' | 'behavior_observation' | 'session_summary';
|
|
115
122
|
export interface Evidence {
|
|
@@ -128,7 +135,7 @@ export interface Evidence {
|
|
|
128
135
|
* evidence 가 어느 host 에서 발생했는지 태그. 미지정 시 'claude' 로 backfill (기존 데이터 호환).
|
|
129
136
|
* core 의 학습 로직은 이 필드를 *호스트별 가중치* 가 아니라 *불일치 demote 신호* 로만 사용한다.
|
|
130
137
|
*/
|
|
131
|
-
host?:
|
|
138
|
+
host?: HostId;
|
|
132
139
|
}
|
|
133
140
|
export interface QualityFacets {
|
|
134
141
|
verification_depth: number;
|
|
@@ -187,7 +194,7 @@ export interface Profile {
|
|
|
187
194
|
* - 'ask': 매번 묻기 (interactive prompt)
|
|
188
195
|
* - undefined: legacy / 미설정 → 'claude' fallback (마이그레이션 호환)
|
|
189
196
|
*/
|
|
190
|
-
default_host?:
|
|
197
|
+
default_host?: HostId | 'ask';
|
|
191
198
|
}
|
|
192
199
|
export type RecommendationSource = 'onboarding' | 'mismatch_recommendation';
|
|
193
200
|
export type RecommendationStatus = 'proposed' | 'accepted' | 'archived';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooojin/forgen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"preferGlobal": true,
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"types": "./dist/lib.d.ts",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@biomejs/biome": "^2.4.16",
|
|
81
81
|
"@types/js-yaml": "^4.0.9",
|
|
82
|
-
"@types/node": "^
|
|
82
|
+
"@types/node": "^26.1.1",
|
|
83
83
|
"@vitest/coverage-v8": "^4.1.7",
|
|
84
84
|
"typescript": "^5.9.3",
|
|
85
85
|
"vitest": "^4.1.7"
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
89
89
|
"js-yaml": "^4.1.1",
|
|
90
|
+
"jsonc-parser": "^3.3.1",
|
|
90
91
|
"zod": "^4.4.3"
|
|
91
92
|
},
|
|
92
93
|
"peerDependencies": {
|
package/plugin.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://claude.ai/schemas/claude-plugin.json",
|
|
3
3
|
"name": "forgen",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Claude Code harness — the more you use Claude, the better it gets",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "jang-ujin",
|
|
8
|
-
"url": "https://github.com/
|
|
8
|
+
"url": "https://github.com/forgen-team"
|
|
9
9
|
},
|
|
10
10
|
"repository": "https://github.com/forgen-team/forgen",
|
|
11
11
|
"homepage": "https://github.com/forgen-team/forgen",
|
|
@@ -65,13 +65,19 @@ PRD 확정 직후 **반드시** `~/.forgen/state/forge-loop.json`에 저장:
|
|
|
65
65
|
mkdir -p ~/.forgen/state
|
|
66
66
|
cat > ~/.forgen/state/forge-loop.json <<EOF
|
|
67
67
|
{"active":true,"startedAt":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","stories":[
|
|
68
|
-
{"id":"US-001","title":"...","passes":false,"attempts":0}
|
|
68
|
+
{"id":"US-001","title":"...","passes":false,"attempts":0,"acceptanceCriteria":["..."]}
|
|
69
69
|
]}
|
|
70
70
|
EOF
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
이 파일이 있어야 Claude가 중간에 멈추지 않도록 Stop 훅이 차단합니다.
|
|
74
|
+
소유 세션은 최초 차단 시점에 Stop 훅이 자동 귀속하므로(`sessionId` 필드) 이
|
|
75
|
+
파일을 쓸 때 세션 ID를 직접 넣을 필요는 없습니다 — 단, 귀속된 세션과 다른
|
|
76
|
+
세션에서는 이 루프가 차단하지 않습니다. 24시간 이상 갱신이 없으면 자동
|
|
77
|
+
해제(1회성 안내 포함)되며, 연속 30회 차단 시에도 안전 상한으로 자동 해제됩니다.
|
|
74
78
|
스토리 완료 시 `passes: true`로 업데이트. 전체 완료는 Stop 훅이 자동 처리.
|
|
79
|
+
`acceptanceCriteria[0]`은 차단 메시지에 `AC1:`로 노출되므로 있으면 첫 항목을
|
|
80
|
+
구체적이고 검증 가능한 문장으로 작성하세요 (없어도 정상 동작).
|
|
75
81
|
|
|
76
82
|
### goal-only 모드 — Phase 1 종료 분기
|
|
77
83
|
|
package/skills/ship/SKILL.md
CHANGED
|
@@ -148,10 +148,27 @@ npm version {patch|minor|major} --no-git-tag-version
|
|
|
148
148
|
|
|
149
149
|
커밋을 주제별 그룹핑 -> CHANGELOG.md 상단에 추가.
|
|
150
150
|
|
|
151
|
+
## Step 6.5: Smoke 증거 생성 (forgen 레포 한정)
|
|
152
|
+
|
|
153
|
+
forgen 레포 자체를 ship 할 때는 릴리스 커밋 전에 smoke 증거를 생성한다
|
|
154
|
+
(ADR-010 W0-2 — CI self-gate 가 릴리스 커밋에서 smoke-report 를 요구).
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# forgen 레포에서만. vitest 는 Step 3 에서 이미 돌았지만 게이트 증거는
|
|
158
|
+
# smoke.cjs 산출물만 인정 — 전체 실행 (실제 프로세스 산출물 원칙).
|
|
159
|
+
if [ -f scripts/smoke.cjs ]; then
|
|
160
|
+
node scripts/smoke.cjs || ABORT
|
|
161
|
+
fi
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- 실패 -> ABORT ("smoke 증거 생성 실패 — 게이트 통과 불가")
|
|
165
|
+
|
|
151
166
|
## Step 7: 릴리스 커밋
|
|
152
167
|
|
|
153
168
|
```bash
|
|
154
|
-
git add package.json CHANGELOG.md
|
|
169
|
+
git add package.json package-lock.json CHANGELOG.md
|
|
170
|
+
# forgen 레포: smoke 증거 포함
|
|
171
|
+
[ -f .forgen-release/smoke-report.json ] && git add .forgen-release/smoke-report.json
|
|
155
172
|
git commit -m "release: v{version}"
|
|
156
173
|
```
|
|
157
174
|
|