@wooojin/forgen 0.4.12 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +109 -0
- package/README.ja.md +4 -4
- package/README.md +89 -27
- package/README.zh.md +4 -4
- package/assets/claude/commands/forge-loop.md +7 -1
- package/assets/claude/commands/ship.md +18 -1
- package/assets/opencode/forgen.ts +52 -0
- package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
- package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
- package/dist/checks/_shared/model-profile.d.ts +25 -0
- package/dist/checks/_shared/model-profile.js +63 -0
- package/dist/cli.js +99 -153
- package/dist/core/auto-compound-runner.d.ts +0 -11
- package/dist/core/auto-compound-runner.js +127 -79
- package/dist/core/compound-consent.d.ts +15 -0
- package/dist/core/compound-consent.js +48 -0
- package/dist/core/compound-sweep-cli.d.ts +57 -0
- package/dist/core/compound-sweep-cli.js +354 -0
- package/dist/core/config-injector.d.ts +16 -1
- package/dist/core/config-injector.js +47 -28
- package/dist/core/dashboard-cli.js +40 -16
- package/dist/core/dashboard.d.ts +3 -4
- package/dist/core/dashboard.js +15 -34
- package/dist/core/dev-cli.d.ts +13 -0
- package/dist/core/dev-cli.js +70 -0
- package/dist/core/doctor.d.ts +16 -5
- package/dist/core/doctor.js +161 -176
- package/dist/core/drift-score.d.ts +2 -0
- package/dist/core/drift-score.js +9 -1
- package/dist/core/harness.js +115 -43
- package/dist/core/health-cli.d.ts +2 -0
- package/dist/core/health-cli.js +6 -1
- package/dist/core/host-detect.d.ts +3 -1
- package/dist/core/host-detect.js +26 -1
- package/dist/core/migrate-cli.js +15 -0
- package/dist/core/migrate-evidence-host.d.ts +2 -1
- package/dist/core/migrate-tenetx.d.ts +50 -0
- package/dist/core/migrate-tenetx.js +262 -0
- package/dist/core/probe-workflow-cli.d.ts +3 -3
- package/dist/core/probe-workflow-cli.js +13 -13
- package/dist/core/recall-cli.js +1 -1
- package/dist/core/regress-map-cli.js +1 -1
- package/dist/core/rendered-rules-manifest.d.ts +29 -0
- package/dist/core/rendered-rules-manifest.js +60 -0
- package/dist/core/session-store.js +14 -3
- package/dist/core/settings-injector.d.ts +3 -0
- package/dist/core/settings-injector.js +12 -16
- package/dist/core/spawn.d.ts +11 -1
- package/dist/core/spawn.js +79 -7
- package/dist/core/state-gc.js +1 -0
- package/dist/core/status-cli.d.ts +20 -0
- package/dist/core/status-cli.js +100 -0
- package/dist/core/statusline-cli.d.ts +7 -0
- package/dist/core/statusline-cli.js +63 -19
- package/dist/core/transcript-summary.d.ts +18 -0
- package/dist/core/transcript-summary.js +81 -0
- package/dist/core/trust-layer-intent.d.ts +21 -1
- package/dist/core/trust-layer-intent.js +7 -0
- package/dist/core/types.d.ts +3 -2
- package/dist/core/uninstall.js +12 -0
- package/dist/core/usage-telemetry.d.ts +7 -1
- package/dist/core/usage-telemetry.js +7 -9
- package/dist/core/v1-bootstrap.js +1 -1
- package/dist/core/watch-cli.js +1 -1
- package/dist/engine/compound-extractor.js +10 -0
- package/dist/engine/compound-loop.js +35 -6
- package/dist/engine/compound-share.d.ts +85 -0
- package/dist/engine/compound-share.js +606 -0
- package/dist/engine/correction-cluster-runner.d.ts +38 -0
- package/dist/engine/correction-cluster-runner.js +188 -0
- package/dist/engine/correction-clustering.d.ts +80 -0
- package/dist/engine/correction-clustering.js +167 -0
- package/dist/engine/enforce-classifier.d.ts +10 -1
- package/dist/engine/enforce-classifier.js +111 -22
- package/dist/engine/extraction-session.js +10 -3
- package/dist/engine/private-filter.d.ts +36 -0
- package/dist/engine/private-filter.js +100 -0
- package/dist/engine/ranking-pipeline.js +4 -2
- package/dist/engine/relevance-gate.d.ts +12 -0
- package/dist/engine/relevance-gate.js +12 -0
- package/dist/engine/roi-demotion.d.ts +79 -0
- package/dist/engine/roi-demotion.js +159 -0
- package/dist/engine/solution-format.d.ts +1 -0
- package/dist/engine/solution-format.js +26 -0
- package/dist/engine/solution-matcher.js +10 -1
- package/dist/fgx.js +19 -6
- package/dist/forge/cli.js +8 -2
- package/dist/hooks/compound-reflection.js +6 -1
- package/dist/hooks/context-guard.d.ts +16 -1
- package/dist/hooks/context-guard.js +92 -46
- package/dist/hooks/post-tool-use.js +2 -3
- package/dist/hooks/pre-compact.js +14 -0
- package/dist/hooks/pre-tool-use.js +5 -1
- package/dist/hooks/shared/stop-triggers.d.ts +29 -2
- package/dist/hooks/shared/stop-triggers.js +35 -2
- package/dist/hooks/solution-injector.d.ts +28 -0
- package/dist/hooks/solution-injector.js +126 -28
- package/dist/hooks/stop-guard.js +5 -2
- package/dist/hooks/subagent-stop-guard.js +4 -1
- package/dist/host/capabilities-claude.js +1 -0
- package/dist/host/capabilities-codex.js +1 -0
- package/dist/host/capabilities-opencode.d.ts +26 -0
- package/dist/host/capabilities-opencode.js +78 -0
- package/dist/host/capabilities-registry.d.ts +7 -0
- package/dist/host/capabilities-registry.js +14 -0
- package/dist/host/exec-host.d.ts +4 -3
- package/dist/host/exec-host.js +2 -0
- package/dist/host/host-binding.d.ts +27 -0
- package/dist/host/host-binding.js +11 -0
- package/dist/host/host-runtime.js +18 -0
- package/dist/host/install-codex.d.ts +8 -0
- package/dist/host/install-codex.js +2 -2
- package/dist/host/install-opencode.d.ts +38 -0
- package/dist/host/install-opencode.js +148 -0
- package/dist/host/install-orchestrator.d.ts +4 -1
- package/dist/host/install-orchestrator.js +20 -1
- package/dist/host/invoke-agent.d.ts +3 -2
- package/dist/host/opencode/context-cli.d.ts +15 -0
- package/dist/host/opencode/context-cli.js +25 -0
- package/dist/host/opencode/guard-cli.d.ts +13 -0
- package/dist/host/opencode/guard-cli.js +39 -0
- package/dist/host/opencode/plugin/forgen.d.ts +31 -0
- package/dist/host/opencode/plugin/forgen.js +60 -0
- package/dist/host/opencode/translate.d.ts +49 -0
- package/dist/host/opencode/translate.js +96 -0
- package/dist/host/parity-harness.d.ts +10 -2
- package/dist/host/projection.js +11 -0
- package/dist/mcp/tools.js +17 -4
- package/dist/store/evidence-store.d.ts +2 -6
- package/dist/store/evidence-store.js +68 -14
- package/dist/store/host-mismatch.d.ts +2 -1
- package/dist/store/host-mismatch.js +8 -8
- package/dist/store/profile-store.d.ts +3 -2
- package/dist/store/types.d.ts +9 -2
- package/package.json +3 -2
- package/plugin.json +2 -2
- package/skills/forge-loop/SKILL.md +7 -1
- package/skills/ship/SKILL.md +18 -1
|
@@ -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
|
+
}
|
package/dist/host/exec-host.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 호환성: 기존 'claude -p prompt --model haiku' 호출은 default_host 가 'claude' 인
|
|
10
10
|
* 경우 동일 동작. Codex 메인 사용자는 자동으로 codex exec --json 호출.
|
|
11
11
|
*/
|
|
12
|
+
import type { HostId } from '../core/trust-layer-intent.js';
|
|
12
13
|
export interface ExecHostOptions {
|
|
13
14
|
/** prompt — `-p`/`exec` 의 본문 */
|
|
14
15
|
prompt: string;
|
|
@@ -19,7 +20,7 @@ export interface ExecHostOptions {
|
|
|
19
20
|
/** working directory */
|
|
20
21
|
cwd?: string;
|
|
21
22
|
/** explicit host override (default: profile.default_host). */
|
|
22
|
-
host?:
|
|
23
|
+
host?: HostId;
|
|
23
24
|
/** ENV vars 추가 (기존 process.env 위에 머지) */
|
|
24
25
|
env?: NodeJS.ProcessEnv;
|
|
25
26
|
}
|
|
@@ -30,10 +31,10 @@ export interface ExecHostOptions {
|
|
|
30
31
|
* 30s default 가 false-positive ETIMEDOUT 발생. 90s 마진 필요.
|
|
31
32
|
* - 명시 timeout 옵션은 그대로 우선.
|
|
32
33
|
*/
|
|
33
|
-
export declare const DEFAULT_TIMEOUT_BY_HOST: Record<
|
|
34
|
+
export declare const DEFAULT_TIMEOUT_BY_HOST: Record<HostId, number>;
|
|
34
35
|
export interface ExecHostResult {
|
|
35
36
|
message: string;
|
|
36
|
-
host:
|
|
37
|
+
host: HostId;
|
|
37
38
|
/** 토큰 사용량 (codex 만 노출. claude 는 null). */
|
|
38
39
|
usage: {
|
|
39
40
|
input_tokens?: number;
|
package/dist/host/exec-host.js
CHANGED
|
@@ -22,6 +22,8 @@ import { parseCodexJsonlOutput } from './codex-output-parser.js';
|
|
|
22
22
|
export const DEFAULT_TIMEOUT_BY_HOST = {
|
|
23
23
|
claude: 30_000,
|
|
24
24
|
codex: 90_000,
|
|
25
|
+
// opencode headless CLI: reasoning-mode 응답이 codex 처럼 길 수 있어 넉넉히(P1 실측 후 조정).
|
|
26
|
+
opencode: 90_000,
|
|
25
27
|
};
|
|
26
28
|
/**
|
|
27
29
|
* 실 host CLI 를 호출하여 prompt 응답 받기.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HostBinding — Multi-Harness Adapter Plan P0 (`reports/harness-probe/adapter-plan-2026-07-20.md` §4.1)
|
|
3
|
+
*
|
|
4
|
+
* `prepareHarness` 의 host if-ladder (`harness.ts:447-479`) 를 formalize 한 계약.
|
|
5
|
+
* 각 host 는 capabilities 선언(`capabilities-registry.ts`) + projection 함수(`projection.ts`) +
|
|
6
|
+
* 세션 준비 단계(hook-surface wiring, 구 if-ladder 분기)를 하나의 등록 단위로 묶는다.
|
|
7
|
+
*
|
|
8
|
+
* `Record<HostId, HostBinding>` 완전성 요구로, 새 host 를 `HostId` 에 추가하면 registry
|
|
9
|
+
* 엔트리 누락이 컴파일 타임에 걸린다 (§4.2 P1 OpenCode 확장의 전제 조건).
|
|
10
|
+
*/
|
|
11
|
+
import type { HostCapabilities, HostId } from '../core/trust-layer-intent.js';
|
|
12
|
+
import type { ProjectToClaudeEvent } from './projection.js';
|
|
13
|
+
import type { V1BootstrapResult } from '../core/v1-bootstrap.js';
|
|
14
|
+
/** `prepareHarness` 가 host-specific 세션 준비 단계에 넘기는 컨텍스트. */
|
|
15
|
+
export interface HarnessSessionContext {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly pkgRoot: string;
|
|
18
|
+
readonly env: Record<string, string>;
|
|
19
|
+
readonly v1Result: V1BootstrapResult;
|
|
20
|
+
}
|
|
21
|
+
export interface HostBinding {
|
|
22
|
+
readonly id: HostId;
|
|
23
|
+
readonly capabilities: HostCapabilities;
|
|
24
|
+
readonly projection: ProjectToClaudeEvent;
|
|
25
|
+
/** harness.ts 의 host-specific artifact 준비 단계 (구 if-ladder 분기 대체). */
|
|
26
|
+
prepareSession(ctx: HarnessSessionContext): Promise<void>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HostBinding — Multi-Harness Adapter Plan P0 (`reports/harness-probe/adapter-plan-2026-07-20.md` §4.1)
|
|
3
|
+
*
|
|
4
|
+
* `prepareHarness` 의 host if-ladder (`harness.ts:447-479`) 를 formalize 한 계약.
|
|
5
|
+
* 각 host 는 capabilities 선언(`capabilities-registry.ts`) + projection 함수(`projection.ts`) +
|
|
6
|
+
* 세션 준비 단계(hook-surface wiring, 구 if-ladder 분기)를 하나의 등록 단위로 묶는다.
|
|
7
|
+
*
|
|
8
|
+
* `Record<HostId, HostBinding>` 완전성 요구로, 새 host 를 `HostId` 에 추가하면 registry
|
|
9
|
+
* 엔트리 누락이 컴파일 타임에 걸린다 (§4.2 P1 OpenCode 확장의 전제 조건).
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -41,9 +41,27 @@ const codexRuntime = {
|
|
|
41
41
|
hookInjectionStrategy: 'generate',
|
|
42
42
|
dangerousSkipFlag: '--dangerously-bypass-approvals-and-sandbox',
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* OpenCode runtime — P1 파운데이션 스텁. OpenCode 는 subprocess-hook 이 아니라 in-process
|
|
46
|
+
* plugin 형태라(plan §2.2), hook wrapping 은 plugin 슬림(`install-opencode` 미구현)이 담당한다.
|
|
47
|
+
* launcher(headless `opencode` CLI)와 라벨만 실값; wrapHookCommand 는 슬림 착지 전까지 fail-loud.
|
|
48
|
+
*/
|
|
49
|
+
const opencodeRuntime = {
|
|
50
|
+
id: 'opencode',
|
|
51
|
+
displayName: 'OpenCode',
|
|
52
|
+
launcher: 'opencode',
|
|
53
|
+
missingInstallMessage: 'OpenCode is not installed.',
|
|
54
|
+
wrapHookCommand() {
|
|
55
|
+
throw new Error('[forgen] OpenCode hook wrapping은 in-process plugin 슬림(install-opencode)이 담당합니다 — P1 미구현.');
|
|
56
|
+
},
|
|
57
|
+
// in-process plugin 이라 subprocess hook 생성 전략과 무관. 슬림 착지 시 재정의.
|
|
58
|
+
hookInjectionStrategy: 'generate',
|
|
59
|
+
dangerousSkipFlag: '',
|
|
60
|
+
};
|
|
44
61
|
const RUNTIMES = {
|
|
45
62
|
claude: claudeRuntime,
|
|
46
63
|
codex: codexRuntime,
|
|
64
|
+
opencode: opencodeRuntime,
|
|
47
65
|
};
|
|
48
66
|
export function getHostRuntime(runtime) {
|
|
49
67
|
const r = RUNTIMES[runtime];
|
|
@@ -46,3 +46,11 @@ export interface CodexInstallResult {
|
|
|
46
46
|
devGuideSkillsRemoved: number;
|
|
47
47
|
}
|
|
48
48
|
export declare function planCodexInstall(opts: CodexInstallOptions): CodexInstallResult;
|
|
49
|
+
export declare function resolveAgentsMdPath(_pkgRoot: string): string;
|
|
50
|
+
export declare function upsertForgenRulesInAgentsMd(opts: {
|
|
51
|
+
agentsMdPath: string;
|
|
52
|
+
pkgRoot: string;
|
|
53
|
+
dryRun: boolean;
|
|
54
|
+
}): {
|
|
55
|
+
injected: boolean;
|
|
56
|
+
};
|
|
@@ -273,7 +273,7 @@ function installCodexSkills(opts) {
|
|
|
273
273
|
return { installed: count };
|
|
274
274
|
}
|
|
275
275
|
// ── P3-3: AGENTS.md inject ────────────────────────────────────────────
|
|
276
|
-
function resolveAgentsMdPath(_pkgRoot) {
|
|
276
|
+
export function resolveAgentsMdPath(_pkgRoot) {
|
|
277
277
|
// Phase 3 critic fix: pkgRoot 기반 walk-up 은 `npm install -g` 시 시스템 디렉토리
|
|
278
278
|
// (예: /usr/local/lib/node_modules/forgen) 에 fallback AGENTS.md 작성 위험.
|
|
279
279
|
// *cwd 기반* 으로 변경 — 사용자 작업 디렉토리의 git root, 없으면 cwd 자체.
|
|
@@ -313,7 +313,7 @@ function buildForgenRulesBlock(pkgRoot) {
|
|
|
313
313
|
];
|
|
314
314
|
return lines.join('\n');
|
|
315
315
|
}
|
|
316
|
-
function upsertForgenRulesInAgentsMd(opts) {
|
|
316
|
+
export function upsertForgenRulesInAgentsMd(opts) {
|
|
317
317
|
const { agentsMdPath, pkgRoot, dryRun } = opts;
|
|
318
318
|
const block = buildForgenRulesBlock(pkgRoot);
|
|
319
319
|
let current = '';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-opencode — forgen 을 OpenCode host 에 설치 (W3-3 P1).
|
|
3
|
+
*
|
|
4
|
+
* OpenCode 는 in-process TS plugin + opencode.json(c) MCP + AGENTS.md 를 쓴다:
|
|
5
|
+
* 1. plugin: `~/.config/opencode/plugins/forgen.ts` (assets/opencode/forgen.ts 배포).
|
|
6
|
+
* GUARD_CMD 는 절대 forgen CLI 경로로 치환(런타임 PATH 비의존 — 리뷰 MED4).
|
|
7
|
+
* 2. MCP: 기존 config 파일(opencode.jsonc 우선, 없으면 opencode.json)에 mcp.forgen-compound
|
|
8
|
+
* surgical 병합. **JSONC 파서(jsonc-parser)로 주석/trailing-comma 보존**, 쓰기 전 백업
|
|
9
|
+
* (리뷰 HIGH: JSON.parse 가 공식 JSONC 를 clobber 하던 데이터 손실 수정).
|
|
10
|
+
* 3. AGENTS.md: cwd 의 forgen rules block (Codex 헬퍼 재사용).
|
|
11
|
+
*/
|
|
12
|
+
export interface OpencodeInstallOptions {
|
|
13
|
+
pkgRoot: string;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
registerMcp?: boolean;
|
|
16
|
+
/** ~/.config/opencode override (격리 테스트용). */
|
|
17
|
+
opencodeConfigDir?: string;
|
|
18
|
+
/** AGENTS.md 위치 override (격리 테스트용). */
|
|
19
|
+
agentsMdPath?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface OpencodeInstallResult {
|
|
22
|
+
configDir: string;
|
|
23
|
+
pluginPath: string;
|
|
24
|
+
pluginInstalled: boolean;
|
|
25
|
+
/** 사용자 소유(비-managed) plugin 을 백업했으면 그 경로. */
|
|
26
|
+
pluginBackupPath?: string;
|
|
27
|
+
mcpRegistered: boolean;
|
|
28
|
+
mcpAlreadyPresent: boolean;
|
|
29
|
+
/** MCP 대상 config 파일(.jsonc 우선). */
|
|
30
|
+
mcpConfigPath: string;
|
|
31
|
+
/** 파싱 불가 config 라 MCP 를 건드리지 않고 abort 했으면 true(데이터 손실 방지). */
|
|
32
|
+
mcpSkippedUnparseable: boolean;
|
|
33
|
+
/** config 백업 경로(수정 전 .bak). */
|
|
34
|
+
mcpBackupPath?: string;
|
|
35
|
+
agentsMdInjected: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function resolveOpencodeConfigDir(opts: OpencodeInstallOptions): string;
|
|
38
|
+
export declare function planOpencodeInstall(opts: OpencodeInstallOptions): OpencodeInstallResult;
|