@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.
Files changed (166) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +261 -0
  3. package/README.ja.md +16 -12
  4. package/README.ko.md +12 -8
  5. package/README.md +90 -27
  6. package/README.zh.md +16 -12
  7. package/assets/claude/commands/forge-loop.md +7 -1
  8. package/assets/claude/commands/ship.md +18 -1
  9. package/assets/opencode/forgen.ts +52 -0
  10. package/assets/shared/hook-registry.json +210 -22
  11. package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
  12. package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
  13. package/dist/checks/_shared/model-profile.d.ts +25 -0
  14. package/dist/checks/_shared/model-profile.js +63 -0
  15. package/dist/cli.js +99 -153
  16. package/dist/core/auto-compound-runner.d.ts +0 -11
  17. package/dist/core/auto-compound-runner.js +199 -92
  18. package/dist/core/changelog-cli.d.ts +4 -1
  19. package/dist/core/changelog-cli.js +8 -7
  20. package/dist/core/compound-consent.d.ts +15 -0
  21. package/dist/core/compound-consent.js +48 -0
  22. package/dist/core/compound-sweep-cli.d.ts +60 -0
  23. package/dist/core/compound-sweep-cli.js +374 -0
  24. package/dist/core/config-injector.d.ts +16 -1
  25. package/dist/core/config-injector.js +47 -28
  26. package/dist/core/dashboard-cli.js +40 -16
  27. package/dist/core/dashboard.d.ts +3 -4
  28. package/dist/core/dashboard.js +15 -34
  29. package/dist/core/dev-cli.d.ts +13 -0
  30. package/dist/core/dev-cli.js +70 -0
  31. package/dist/core/doctor.d.ts +16 -5
  32. package/dist/core/doctor.js +190 -176
  33. package/dist/core/drift-score.d.ts +2 -0
  34. package/dist/core/drift-score.js +9 -1
  35. package/dist/core/harness.js +115 -43
  36. package/dist/core/health-cli.d.ts +2 -0
  37. package/dist/core/health-cli.js +6 -1
  38. package/dist/core/host-detect.d.ts +3 -1
  39. package/dist/core/host-detect.js +26 -1
  40. package/dist/core/migrate-cli.js +15 -0
  41. package/dist/core/migrate-evidence-host.d.ts +2 -1
  42. package/dist/core/migrate-tenetx.d.ts +50 -0
  43. package/dist/core/migrate-tenetx.js +262 -0
  44. package/dist/core/probe-workflow-cli.d.ts +3 -3
  45. package/dist/core/probe-workflow-cli.js +13 -13
  46. package/dist/core/recall-cli.js +1 -1
  47. package/dist/core/regress-map-cli.js +1 -1
  48. package/dist/core/rendered-rules-manifest.d.ts +29 -0
  49. package/dist/core/rendered-rules-manifest.js +60 -0
  50. package/dist/core/session-store.js +14 -3
  51. package/dist/core/settings-injector.d.ts +3 -0
  52. package/dist/core/settings-injector.js +12 -16
  53. package/dist/core/spawn.d.ts +37 -1
  54. package/dist/core/spawn.js +116 -9
  55. package/dist/core/state-gc.js +1 -0
  56. package/dist/core/status-cli.d.ts +20 -0
  57. package/dist/core/status-cli.js +100 -0
  58. package/dist/core/statusline-cli.d.ts +7 -0
  59. package/dist/core/statusline-cli.js +63 -19
  60. package/dist/core/transcript-summary.d.ts +18 -0
  61. package/dist/core/transcript-summary.js +81 -0
  62. package/dist/core/trust-layer-intent.d.ts +21 -1
  63. package/dist/core/trust-layer-intent.js +7 -0
  64. package/dist/core/types.d.ts +3 -2
  65. package/dist/core/uninstall.js +12 -0
  66. package/dist/core/usage-telemetry.d.ts +7 -1
  67. package/dist/core/usage-telemetry.js +7 -9
  68. package/dist/core/v1-bootstrap.js +1 -1
  69. package/dist/core/watch-cli.js +1 -1
  70. package/dist/engine/compound-extractor.js +10 -0
  71. package/dist/engine/compound-loop.js +55 -7
  72. package/dist/engine/compound-share.d.ts +85 -0
  73. package/dist/engine/compound-share.js +606 -0
  74. package/dist/engine/correction-cluster-runner.d.ts +38 -0
  75. package/dist/engine/correction-cluster-runner.js +188 -0
  76. package/dist/engine/correction-clustering.d.ts +80 -0
  77. package/dist/engine/correction-clustering.js +167 -0
  78. package/dist/engine/enforce-classifier.d.ts +10 -1
  79. package/dist/engine/enforce-classifier.js +111 -22
  80. package/dist/engine/extraction-session.js +10 -3
  81. package/dist/engine/private-filter.d.ts +36 -0
  82. package/dist/engine/private-filter.js +100 -0
  83. package/dist/engine/ranking-pipeline.js +4 -2
  84. package/dist/engine/relevance-gate.d.ts +12 -0
  85. package/dist/engine/relevance-gate.js +12 -0
  86. package/dist/engine/roi-demotion.d.ts +79 -0
  87. package/dist/engine/roi-demotion.js +159 -0
  88. package/dist/engine/solution-format.d.ts +1 -0
  89. package/dist/engine/solution-format.js +26 -0
  90. package/dist/engine/solution-matcher.js +10 -1
  91. package/dist/fgx.js +7 -6
  92. package/dist/forge/cli.js +8 -2
  93. package/dist/hooks/compound-reflection.js +6 -1
  94. package/dist/hooks/context-guard.d.ts +16 -1
  95. package/dist/hooks/context-guard.js +99 -49
  96. package/dist/hooks/hook-config.js +5 -0
  97. package/dist/hooks/hook-registry.d.ts +6 -1
  98. package/dist/hooks/hooks-generator.js +3 -0
  99. package/dist/hooks/post-tool-use.js +2 -3
  100. package/dist/hooks/pre-compact.js +14 -0
  101. package/dist/hooks/pre-tool-use.js +5 -1
  102. package/dist/hooks/session-end.d.ts +34 -0
  103. package/dist/hooks/session-end.js +100 -0
  104. package/dist/hooks/session-recovery.js +18 -1
  105. package/dist/hooks/shared/hook-timing.js +6 -1
  106. package/dist/hooks/shared/stop-triggers.d.ts +29 -2
  107. package/dist/hooks/shared/stop-triggers.js +35 -2
  108. package/dist/hooks/solution-injector.d.ts +28 -0
  109. package/dist/hooks/solution-injector.js +126 -28
  110. package/dist/hooks/stop-guard.js +5 -2
  111. package/dist/hooks/subagent-stop-guard.js +4 -1
  112. package/dist/host/capabilities-claude.js +1 -0
  113. package/dist/host/capabilities-codex.js +1 -0
  114. package/dist/host/capabilities-opencode.d.ts +26 -0
  115. package/dist/host/capabilities-opencode.js +78 -0
  116. package/dist/host/capabilities-registry.d.ts +7 -0
  117. package/dist/host/capabilities-registry.js +14 -0
  118. package/dist/host/codex-rules-context.d.ts +25 -0
  119. package/dist/host/codex-rules-context.js +62 -0
  120. package/dist/host/exec-host.d.ts +14 -3
  121. package/dist/host/exec-host.js +15 -2
  122. package/dist/host/host-binding.d.ts +27 -0
  123. package/dist/host/host-binding.js +11 -0
  124. package/dist/host/host-runtime.js +18 -0
  125. package/dist/host/install-codex.d.ts +66 -0
  126. package/dist/host/install-codex.js +293 -25
  127. package/dist/host/install-opencode.d.ts +38 -0
  128. package/dist/host/install-opencode.js +148 -0
  129. package/dist/host/install-orchestrator.d.ts +4 -1
  130. package/dist/host/install-orchestrator.js +30 -1
  131. package/dist/host/invoke-agent.d.ts +3 -2
  132. package/dist/host/invoke-agent.js +1 -0
  133. package/dist/host/opencode/context-cli.d.ts +15 -0
  134. package/dist/host/opencode/context-cli.js +25 -0
  135. package/dist/host/opencode/guard-cli.d.ts +13 -0
  136. package/dist/host/opencode/guard-cli.js +39 -0
  137. package/dist/host/opencode/plugin/forgen.d.ts +31 -0
  138. package/dist/host/opencode/plugin/forgen.js +60 -0
  139. package/dist/host/opencode/translate.d.ts +49 -0
  140. package/dist/host/opencode/translate.js +96 -0
  141. package/dist/host/parity-harness.d.ts +10 -2
  142. package/dist/host/parity-harness.js +7 -3
  143. package/dist/host/projection.d.ts +37 -3
  144. package/dist/host/projection.js +104 -42
  145. package/dist/mcp/tools.js +17 -4
  146. package/dist/store/evidence-store.d.ts +2 -6
  147. package/dist/store/evidence-store.js +164 -46
  148. package/dist/store/host-mismatch.d.ts +2 -1
  149. package/dist/store/host-mismatch.js +8 -8
  150. package/dist/store/profile-store.d.ts +3 -2
  151. package/dist/store/rule-store.d.ts +41 -0
  152. package/dist/store/rule-store.js +77 -2
  153. package/dist/store/types.d.ts +9 -2
  154. package/hooks/hooks.json +13 -1
  155. package/package.json +3 -2
  156. package/plugin.json +2 -2
  157. package/skills/architecture-decision/SKILL.md +18 -0
  158. package/skills/calibrate/SKILL.md +18 -0
  159. package/skills/code-review/SKILL.md +17 -0
  160. package/skills/compound/SKILL.md +17 -0
  161. package/skills/deep-interview/SKILL.md +7 -0
  162. package/skills/docker/SKILL.md +18 -0
  163. package/skills/forge-loop/SKILL.md +30 -1
  164. package/skills/learn/SKILL.md +15 -0
  165. package/skills/retro/SKILL.md +16 -0
  166. package/skills/ship/SKILL.md +36 -1
@@ -9,7 +9,13 @@
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 {
14
+ /**
15
+ * ADR-015 C-G1: forgen 훅을 끄고(FORGEN_NESTED_RUN=1) 세션을 남기지 않는 "추출용 중첩 실행" 모드.
16
+ * 기본 true. 위임 에이전트(invoke-agent)처럼 훅이 살아 있어야 하는 호출은 false.
17
+ */
18
+ nestedRun?: boolean;
13
19
  /** prompt — `-p`/`exec` 의 본문 */
14
20
  prompt: string;
15
21
  /** model 힌트 (claude: --model haiku, codex: 무시 — codex CLI 가 default 사용) */
@@ -19,7 +25,7 @@ export interface ExecHostOptions {
19
25
  /** working directory */
20
26
  cwd?: string;
21
27
  /** explicit host override (default: profile.default_host). */
22
- host?: 'claude' | 'codex';
28
+ host?: HostId;
23
29
  /** ENV vars 추가 (기존 process.env 위에 머지) */
24
30
  env?: NodeJS.ProcessEnv;
25
31
  }
@@ -30,10 +36,10 @@ export interface ExecHostOptions {
30
36
  * 30s default 가 false-positive ETIMEDOUT 발생. 90s 마진 필요.
31
37
  * - 명시 timeout 옵션은 그대로 우선.
32
38
  */
33
- export declare const DEFAULT_TIMEOUT_BY_HOST: Record<'claude' | 'codex', number>;
39
+ export declare const DEFAULT_TIMEOUT_BY_HOST: Record<HostId, number>;
34
40
  export interface ExecHostResult {
35
41
  message: string;
36
- host: 'claude' | 'codex';
42
+ host: HostId;
37
43
  /** 토큰 사용량 (codex 만 노출. claude 는 null). */
38
44
  usage: {
39
45
  input_tokens?: number;
@@ -49,6 +55,11 @@ export interface ExecHostResult {
49
55
  * 보장 (사용자 환경 미오염). compound-extractor / auto-compound-runner 같은
50
56
  * 백그라운드 학습 호출에 적합.
51
57
  */
58
+ /** ADR-015 C-G1 — forgen 이 띄우는 중첩 claude 실행의 공통 표식. hook-config.isHookEnabled 가 읽는다. */
59
+ export declare const NESTED_RUN_ENV: Readonly<Record<string, string>>;
60
+ /** `--no-session-persistence` 는 --print 전용 — 추출 run 의 transcript 를 디스크에 남기지 않는다. */
61
+ export declare const NESTED_RUN_CLAUDE_ARGS: ReadonlyArray<string>;
62
+ export declare function withNestedRunClaudeArgs(args: string[]): string[];
52
63
  export declare function execHost(opts: ExecHostOptions): ExecHostResult;
53
64
  /** 1회 retry — transient 에러(ETIMEDOUT 등) 대응. */
54
65
  export declare function execHostRetry(opts: ExecHostOptions): ExecHostResult;
@@ -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 응답 받기.
@@ -32,6 +34,13 @@ export const DEFAULT_TIMEOUT_BY_HOST = {
32
34
  * 보장 (사용자 환경 미오염). compound-extractor / auto-compound-runner 같은
33
35
  * 백그라운드 학습 호출에 적합.
34
36
  */
37
+ /** ADR-015 C-G1 — forgen 이 띄우는 중첩 claude 실행의 공통 표식. hook-config.isHookEnabled 가 읽는다. */
38
+ export const NESTED_RUN_ENV = { FORGEN_NESTED_RUN: '1' };
39
+ /** `--no-session-persistence` 는 --print 전용 — 추출 run 의 transcript 를 디스크에 남기지 않는다. */
40
+ export const NESTED_RUN_CLAUDE_ARGS = ['--no-session-persistence'];
41
+ export function withNestedRunClaudeArgs(args) {
42
+ return args.includes('--no-session-persistence') ? args : [...args, ...NESTED_RUN_CLAUDE_ARGS];
43
+ }
35
44
  export function execHost(opts) {
36
45
  const resolved = resolveDefaultHost(opts.host);
37
46
  // 'ask' 는 자동 호출 컨텍스트라 명시 fallback. 그러나 Codex-only 사용자가 'ask'
@@ -51,10 +60,14 @@ export function execHost(opts) {
51
60
  env: { ...process.env, ...(opts.env ?? {}) },
52
61
  };
53
62
  if (host === 'claude') {
54
- const args = ['-p', opts.prompt];
63
+ // ADR-015 C-G1: 중첩 실행 표식 + 세션 비영속 (추출 run 의 transcript 가 다음 SessionStart 의
64
+ // "이전 세션 auto-compound" 후보로 잡히거나 forgen 훅이 재귀 발화하지 않도록).
65
+ const nested = opts.nestedRun ?? true;
66
+ const args = ['-p', opts.prompt, ...(nested ? NESTED_RUN_CLAUDE_ARGS : [])];
55
67
  if (opts.model)
56
68
  args.push('--model', opts.model);
57
- const stdout = execFileSync('claude', args, baseOpts);
69
+ const env = nested ? { ...(baseOpts.env ?? {}), ...NESTED_RUN_ENV } : baseOpts.env;
70
+ const stdout = execFileSync('claude', args, { ...baseOpts, env });
58
71
  return { message: stdout.toString().trim(), host: 'claude', usage: null };
59
72
  }
60
73
  // host === 'codex'
@@ -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];
@@ -44,5 +44,71 @@ export interface CodexInstallResult {
44
44
  devGuideSkillsPath: string;
45
45
  devGuideSkillsInstalled: number;
46
46
  devGuideSkillsRemoved: number;
47
+ /** ADR-014 D2: ~/.codex/agents/ch-*.toml 커스텀 에이전트 설치 결과 */
48
+ agentsPath: string;
49
+ agentsInstalled: number;
50
+ agentsRemoved: number;
51
+ /** ADR-014 D4: hooks.json forgen 엔트리 중 config.toml hooks.state 에 신뢰 기록이 있는 수 */
52
+ hookTrust: CodexHookTrustAudit;
53
+ /** ADR-014 D2: config.toml `[features] multi_agent = true` 여부 (false 면 ch-* 에이전트 spawn 불가 → 안내) */
54
+ multiAgentEnabled: boolean;
55
+ }
56
+ export interface CodexHookTrustAudit {
57
+ /** hooks.json 의 forgen hook 명령 수 (Codex 가 지원하는 이벤트만) */
58
+ total: number;
59
+ /** Codex 가 모르는 이벤트라 조용히 무시되는 forgen 엔트리 (`<event>:<i>:<j>`) — trust 대상이 아님 */
60
+ ignoredByCodex: string[];
61
+ /** config.toml `[hooks.state."<hooks.json>:<event>:<i>:<j>"]` 에 trusted_hash 가 있는 수 */
62
+ trusted: number;
63
+ /** 신뢰 기록이 없는 hook 키 (`<event>:<i>:<j>`) */
64
+ untrusted: string[];
65
+ /** config.toml 자체가 없거나 hooks.state 가 전혀 없으면 true (Codex 가 아직 한 번도 훅을 review 안 함) */
66
+ noStateRecorded: boolean;
67
+ }
68
+ interface HooksFile {
69
+ description?: string;
70
+ hooks: Record<string, Array<unknown>>;
47
71
  }
48
72
  export declare function planCodexInstall(opts: CodexInstallOptions): CodexInstallResult;
73
+ /** `[features]` 섹션 안에 `multi_agent = true` 가 있는지 (TOML 라이브러리 없이 섹션 범위만 본다). */
74
+ export declare function isCodexMultiAgentEnabled(configToml: string): boolean;
75
+ /**
76
+ * Codex 0.153 hooks 공식 이벤트 12종 (learn.chatgpt.com/docs/hooks + binary 문자열). 이 밖의 이벤트
77
+ * (예: Claude 전용 PostToolUseFailure) 는 Codex 가 조용히 무시하므로 trust 대상이 아니다.
78
+ */
79
+ export declare const CODEX_SUPPORTED_HOOK_EVENTS: ReadonlySet<string>;
80
+ /** Codex 는 hooks.state 키에 이벤트명을 snake_case 로 쓴다 (PreToolUse → pre_tool_use). */
81
+ export declare function codexHookEventKey(event: string): string;
82
+ /**
83
+ * hooks.json 의 forgen 엔트리 각각에 대해 config.toml 의
84
+ * `[hooks.state."<hooksPath>:<event>:<groupIdx>:<hookIdx>"]` 섹션(trusted_hash) 존재를 대조한다.
85
+ * trusted_hash 자체는 검증하지 않는다 — Codex 가 변경된 훅을 review 전까지 skip 하는 정책을
86
+ * forgen 이 우회하지 않기 위해, *기록 유무* 만 가시화한다 (ADR-014 D4).
87
+ */
88
+ export declare function auditCodexHookTrust(opts: {
89
+ hooksPath: string;
90
+ configTomlPath: string;
91
+ pkgRoot: string;
92
+ hooksFile?: HooksFile | null;
93
+ configToml?: string;
94
+ }): CodexHookTrustAudit;
95
+ /**
96
+ * Claude agent .md → Codex agent role TOML.
97
+ * 공식 스키마(필수 name/description/developer_instructions, 선택 model_reasoning_effort/sandbox_mode)
98
+ * 외 필드는 쓰지 않는다 — Codex 가 unknown field 를 거부 (ADR-014 D2).
99
+ */
100
+ export declare function renderCodexAgentToml(file: string, raw: string): {
101
+ name: string;
102
+ toml: string;
103
+ } | null;
104
+ /** ADR-014 D3 — Codex 스킬에는 `$ARGUMENTS` 치환 변수가 없다. */
105
+ export declare function adaptSkillBodyForCodex(body: string): string;
106
+ export declare function resolveAgentsMdPath(_pkgRoot: string): string;
107
+ export declare function upsertForgenRulesInAgentsMd(opts: {
108
+ agentsMdPath: string;
109
+ pkgRoot: string;
110
+ dryRun: boolean;
111
+ }): {
112
+ injected: boolean;
113
+ };
114
+ export {};
@@ -85,29 +85,39 @@ export function planCodexInstall(opts) {
85
85
  releaseMode,
86
86
  });
87
87
  const generatedHooks = generated.hooks;
88
- // 2) 기존 hooks.json 읽기 + forgen entry 제거 후 보존
88
+ // 2) 기존 hooks.json 읽기 — forgen 그룹은 *제자리에서* 교체, 사용자 그룹은 위치 그대로 보존.
89
+ // (0.5.3 critic/실머신: 이전엔 사용자 그룹을 앞으로 모으고 forgen 을 뒤에 붙여 그룹 인덱스가
90
+ // 바뀌었고, Codex 의 trust 키 `<event>:<groupIdx>:<hookIdx>` 가 어긋나 20/21 → 12/21 로
91
+ // 훅 신뢰가 깨졌다. 바이트 동일성이 곧 신뢰 보존이다.)
89
92
  const existing = readJsonFile(hooksPath);
90
93
  const existingHooksByEvent = (existing?.hooks ?? {});
91
- const preserved = {};
94
+ const eventOrder = [...new Set([...Object.keys(existingHooksByEvent), ...Object.keys(generatedHooks)])];
95
+ const merged = {};
92
96
  let preservedCount = 0;
93
- for (const [event, entries] of Object.entries(existingHooksByEvent)) {
94
- if (!Array.isArray(entries))
95
- continue;
96
- const userEntries = entries.filter((e) => !isForgenManagedHook(e, opts.pkgRoot));
97
- if (userEntries.length > 0) {
98
- preserved[event] = userEntries;
99
- preservedCount += userEntries.length;
100
- }
101
- }
102
- // 3) merge: user 보존 + forgen fresh.
103
- // `forgenCount` 는 실제 hook 명령 개수 (matcher group 내부 hooks[] 길이의 합) 로 집계한다.
104
- const merged = { ...preserved };
105
97
  let forgenCount = 0;
106
- for (const [event, entries] of Object.entries(generatedHooks)) {
107
- const list = merged[event] ?? [];
108
- list.push(...entries);
109
- merged[event] = list;
110
- for (const group of entries) {
98
+ for (const event of eventOrder) {
99
+ const existingGroups = Array.isArray(existingHooksByEvent[event]) ? existingHooksByEvent[event] : [];
100
+ const generatedGroups = generatedHooks[event] ?? [];
101
+ const out = [];
102
+ let inserted = false;
103
+ for (const group of existingGroups) {
104
+ if (isForgenManagedHook(group, opts.pkgRoot)) {
105
+ if (!inserted) {
106
+ out.push(...generatedGroups);
107
+ inserted = true;
108
+ }
109
+ // 이후 중복 forgen 그룹은 드롭 (stale 누적 방지)
110
+ }
111
+ else {
112
+ out.push(group);
113
+ preservedCount += 1;
114
+ }
115
+ }
116
+ if (!inserted && generatedGroups.length > 0)
117
+ out.push(...generatedGroups);
118
+ if (out.length > 0)
119
+ merged[event] = out;
120
+ for (const group of generatedGroups) {
111
121
  const g = group;
112
122
  if (Array.isArray(g.hooks))
113
123
  forgenCount += g.hooks.length;
@@ -156,6 +166,21 @@ export function planCodexInstall(opts) {
156
166
  codexHome,
157
167
  dryRun: opts.dryRun ?? false,
158
168
  });
169
+ // 9) ADR-014 D2: assets/claude/agents/*.md → ~/.codex/agents/ch-<name>.toml
170
+ const codexAgents = installCodexAgents({
171
+ sourceDir: path.join(opts.pkgRoot, 'assets', 'claude', 'agents'),
172
+ targetDir: path.join(codexHome, 'agents'),
173
+ dryRun: opts.dryRun ?? false,
174
+ });
175
+ // 10) ADR-014 D4: 훅 신뢰 감사 (dryRun 이면 현재 디스크 상태 기준)
176
+ const hookTrust = auditCodexHookTrust({
177
+ hooksPath,
178
+ configTomlPath,
179
+ pkgRoot: opts.pkgRoot,
180
+ hooksFile: opts.dryRun ? (existing ?? finalHooksFile) : finalHooksFile,
181
+ configToml: mcpContentToWrite ?? (fs.existsSync(configTomlPath) ? fs.readFileSync(configTomlPath, 'utf-8') : ''),
182
+ });
183
+ const multiAgentEnabled = isCodexMultiAgentEnabled(mcpContentToWrite ?? (fs.existsSync(configTomlPath) ? fs.readFileSync(configTomlPath, 'utf-8') : ''));
159
184
  return {
160
185
  codexHome,
161
186
  hooksPath,
@@ -172,8 +197,230 @@ export function planCodexInstall(opts) {
172
197
  devGuideSkillsPath: devGuideResult.devGuideSkillsPath,
173
198
  devGuideSkillsInstalled: devGuideResult.devGuideSkillsInstalled,
174
199
  devGuideSkillsRemoved: devGuideResult.devGuideSkillsRemoved,
200
+ agentsPath: codexAgents.agentsPath,
201
+ agentsInstalled: codexAgents.installed,
202
+ agentsRemoved: codexAgents.removed,
203
+ hookTrust,
204
+ multiAgentEnabled,
175
205
  };
176
206
  }
207
+ /** `[features]` 섹션 안에 `multi_agent = true` 가 있는지 (TOML 라이브러리 없이 섹션 범위만 본다). */
208
+ export function isCodexMultiAgentEnabled(configToml) {
209
+ const m = configToml.match(/^\[features\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m);
210
+ if (!m)
211
+ return false;
212
+ return /^\s*multi_agent\s*=\s*true\s*$/m.test(m[1]);
213
+ }
214
+ // ── ADR-014 D4: Codex hook trust audit ────────────────────────────────
215
+ /**
216
+ * Codex 0.153 hooks 공식 이벤트 12종 (learn.chatgpt.com/docs/hooks + binary 문자열). 이 밖의 이벤트
217
+ * (예: Claude 전용 PostToolUseFailure) 는 Codex 가 조용히 무시하므로 trust 대상이 아니다.
218
+ */
219
+ export const CODEX_SUPPORTED_HOOK_EVENTS = new Set([
220
+ 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact', 'UserPromptSubmit',
221
+ 'SubagentStart', 'SubagentStop', 'Stop', 'Interrupt', 'SessionStart', 'SessionEnd',
222
+ ]);
223
+ /** Codex 는 hooks.state 키에 이벤트명을 snake_case 로 쓴다 (PreToolUse → pre_tool_use). */
224
+ export function codexHookEventKey(event) {
225
+ return event.replace(/(?<!^)([A-Z])/g, '_$1').toLowerCase();
226
+ }
227
+ /**
228
+ * hooks.json 의 forgen 엔트리 각각에 대해 config.toml 의
229
+ * `[hooks.state."<hooksPath>:<event>:<groupIdx>:<hookIdx>"]` 섹션(trusted_hash) 존재를 대조한다.
230
+ * trusted_hash 자체는 검증하지 않는다 — Codex 가 변경된 훅을 review 전까지 skip 하는 정책을
231
+ * forgen 이 우회하지 않기 위해, *기록 유무* 만 가시화한다 (ADR-014 D4).
232
+ */
233
+ export function auditCodexHookTrust(opts) {
234
+ const hooksFile = opts.hooksFile ?? readJsonFile(opts.hooksPath);
235
+ const toml = opts.configToml ?? (fs.existsSync(opts.configTomlPath) ? fs.readFileSync(opts.configTomlPath, 'utf-8') : '');
236
+ const stateKeys = new Set();
237
+ const re = /^\[hooks\.state\."([^"]+)"\]\s*$/gm;
238
+ let m = re.exec(toml);
239
+ while (m !== null) {
240
+ stateKeys.add(m[1]);
241
+ m = re.exec(toml);
242
+ }
243
+ let total = 0;
244
+ let trusted = 0;
245
+ const untrusted = [];
246
+ const ignoredByCodex = [];
247
+ const events = (hooksFile?.hooks ?? {});
248
+ for (const [event, groups] of Object.entries(events)) {
249
+ if (!Array.isArray(groups))
250
+ continue;
251
+ groups.forEach((group, gi) => {
252
+ const g = group;
253
+ if (!Array.isArray(g.hooks))
254
+ return;
255
+ g.hooks.forEach((h, hi) => {
256
+ const isForgen = typeof h.command === 'string' &&
257
+ (h.command.includes(opts.pkgRoot) || FORGEN_HOOK_SCRIPT_MARKER.test(h.command));
258
+ if (!isForgen)
259
+ return;
260
+ const key = `${codexHookEventKey(event)}:${gi}:${hi}`;
261
+ if (!CODEX_SUPPORTED_HOOK_EVENTS.has(event)) {
262
+ ignoredByCodex.push(key);
263
+ return;
264
+ }
265
+ total += 1;
266
+ if (stateKeys.has(`${opts.hooksPath}:${key}`))
267
+ trusted += 1;
268
+ else
269
+ untrusted.push(key);
270
+ });
271
+ });
272
+ }
273
+ return { total, trusted, untrusted, ignoredByCodex, noStateRecorded: stateKeys.size === 0 };
274
+ }
275
+ // ── ADR-014 D2: Codex custom agents (~/.codex/agents/ch-*.toml) ──────
276
+ const AGENT_TOML_MARKER = '# forgen-managed';
277
+ const AGENT_NAME_PREFIX = 'ch-';
278
+ function parseAgentMarkdown(raw) {
279
+ const fm = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
280
+ if (!fm)
281
+ return null;
282
+ const meta = {};
283
+ const tools = [];
284
+ const disallowedTools = [];
285
+ let currentList = null;
286
+ for (const line of fm[1].split('\n')) {
287
+ const listItem = line.match(/^\s+-\s+(.+)$/);
288
+ if (currentList && listItem) {
289
+ currentList.push(listItem[1].trim());
290
+ continue;
291
+ }
292
+ currentList = null;
293
+ const kv = line.match(/^([A-Za-z_]+):\s*(.*)$/);
294
+ if (!kv)
295
+ continue;
296
+ const [, k, v] = kv;
297
+ if (k === 'tools' || k === 'disallowedTools') {
298
+ const target = k === 'tools' ? tools : disallowedTools;
299
+ // 인라인 배열 `tools: [Read, Bash]` 도 허용
300
+ const inline = v.trim().match(/^\[(.*)\]$/);
301
+ if (inline)
302
+ target.push(...inline[1].split(',').map((t) => t.trim()).filter(Boolean));
303
+ else
304
+ currentList = target;
305
+ continue;
306
+ }
307
+ meta[k] = v.trim();
308
+ }
309
+ return { meta, tools, disallowedTools, body: fm[2].trim() };
310
+ }
311
+ /** TOML 문자열에 들어갈 수 없는 제어문자 제거 (탭/개행/CR 은 유지). */
312
+ function stripTomlControlChars(v) {
313
+ let out = '';
314
+ for (const ch of v) {
315
+ const code = ch.charCodeAt(0);
316
+ const isAllowed = (code >= 0x20 && code !== 0x7f) || code === 0x09 || code === 0x0a || code === 0x0d;
317
+ if (isAllowed)
318
+ out += ch;
319
+ }
320
+ return out;
321
+ }
322
+ /** TOML basic string (한 줄). JSON 문자열 문법은 TOML basic string 의 부분집합. */
323
+ function tomlString(v) {
324
+ return JSON.stringify(stripTomlControlChars(v));
325
+ }
326
+ /** TOML multi-line basic string. 백슬래시와 삼중따옴표만 이스케이프하면 된다. */
327
+ function tomlMultiline(v) {
328
+ const cleaned = stripTomlControlChars(v)
329
+ .split('\\').join('\\\\')
330
+ .split('"""').join('\\"\\"\\"');
331
+ return `"""\n${cleaned}\n"""`;
332
+ }
333
+ const REASONING_BY_CLAUDE_MODEL = { opus: 'high', sonnet: 'medium', haiku: 'low' };
334
+ /**
335
+ * Claude agent .md → Codex agent role TOML.
336
+ * 공식 스키마(필수 name/description/developer_instructions, 선택 model_reasoning_effort/sandbox_mode)
337
+ * 외 필드는 쓰지 않는다 — Codex 가 unknown field 를 거부 (ADR-014 D2).
338
+ */
339
+ export function renderCodexAgentToml(file, raw) {
340
+ const parsed = parseAgentMarkdown(raw);
341
+ if (!parsed)
342
+ return null;
343
+ const base = file.replace(/\.md$/, '');
344
+ const name = base.startsWith(AGENT_NAME_PREFIX) ? base : `${AGENT_NAME_PREFIX}${base}`;
345
+ if (!/^[A-Za-z0-9 _-]+$/.test(name))
346
+ return null;
347
+ const description = parsed.meta.description || name;
348
+ // critic 2026-10-01: 7/14 에이전트는 `tools:` 대신 `disallowedTools: [Write, Edit]` 로 읽기전용을 선언.
349
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
350
+ const canWrite = parsed.tools.length > 0
351
+ ? parsed.tools.some((t) => WRITE_TOOLS.has(t))
352
+ : !parsed.disallowedTools.some((t) => WRITE_TOOLS.has(t));
353
+ const sandbox = canWrite ? 'workspace-write' : 'read-only';
354
+ const effort = REASONING_BY_CLAUDE_MODEL[parsed.meta.model ?? ''] ?? 'medium';
355
+ const body = parsed.body.length > 0 ? parsed.body : description;
356
+ const toml = [
357
+ AGENT_TOML_MARKER,
358
+ `# generated by \`forgen install codex\` from assets/claude/agents/${file} — do not edit; re-generated on install`,
359
+ `name = ${tomlString(name)}`,
360
+ `description = ${tomlString(description)}`,
361
+ `model_reasoning_effort = ${tomlString(effort)}`,
362
+ `sandbox_mode = ${tomlString(sandbox)}`,
363
+ `developer_instructions = ${tomlMultiline(body)}`,
364
+ '',
365
+ ].join('\n');
366
+ return { name, toml };
367
+ }
368
+ function installCodexAgents(opts) {
369
+ const { sourceDir, targetDir, dryRun } = opts;
370
+ if (!fs.existsSync(sourceDir))
371
+ return { agentsPath: targetDir, installed: 0, removed: 0 };
372
+ const files = fs.readdirSync(sourceDir).filter((f) => f.endsWith('.md'));
373
+ const isUserOwned = (p) => {
374
+ try {
375
+ const st = fs.lstatSync(p);
376
+ if (st.isSymbolicLink())
377
+ return true; // 사용자 심링크 (dangling 포함) — 건드리지 않음
378
+ return !fs.readFileSync(p, 'utf-8').slice(0, 64).startsWith(AGENT_TOML_MARKER);
379
+ }
380
+ catch {
381
+ return false; // 없음
382
+ }
383
+ };
384
+ if (dryRun) {
385
+ const wouldInstall = files.filter((f) => {
386
+ const r = renderCodexAgentToml(f, fs.readFileSync(path.join(sourceDir, f), 'utf-8'));
387
+ return r !== null && !isUserOwned(path.join(targetDir, `${r.name}.toml`));
388
+ }).length;
389
+ return { agentsPath: targetDir, installed: wouldInstall, removed: 0 };
390
+ }
391
+ fs.mkdirSync(targetDir, { recursive: true });
392
+ // stale 정리: forgen-managed 마커가 있는 ch-*.toml 만. 사용자 파일은 보존.
393
+ let removed = 0;
394
+ for (const entry of fs.readdirSync(targetDir)) {
395
+ if (!entry.startsWith(AGENT_NAME_PREFIX) || !entry.endsWith('.toml'))
396
+ continue;
397
+ const p = path.join(targetDir, entry);
398
+ try {
399
+ if (fs.lstatSync(p).isSymbolicLink())
400
+ continue;
401
+ const head = fs.readFileSync(p, 'utf-8').slice(0, 64);
402
+ if (!head.startsWith(AGENT_TOML_MARKER))
403
+ continue;
404
+ fs.unlinkSync(p);
405
+ removed += 1;
406
+ }
407
+ catch { /* best-effort */ }
408
+ }
409
+ let installed = 0;
410
+ for (const file of files) {
411
+ const rendered = renderCodexAgentToml(file, fs.readFileSync(path.join(sourceDir, file), 'utf-8'));
412
+ if (!rendered)
413
+ continue;
414
+ const dst = path.join(targetDir, `${rendered.name}.toml`);
415
+ if (isUserOwned(dst)) {
416
+ // 방금 stale 정리에서 살아남은 = 사용자 작성 (마커 없음) 또는 심링크 → 보존
417
+ continue;
418
+ }
419
+ fs.writeFileSync(dst, rendered.toml, 'utf-8');
420
+ installed += 1;
421
+ }
422
+ return { agentsPath: targetDir, installed, removed };
423
+ }
177
424
  // ── v0.4.9: dev-guide skills → ~/.codex/skills ────────────────────────
178
425
  // dev-guide prefix pattern: forgen-<stack>-<skill> (e.g. forgen-react-fe-build)
179
426
  // 반드시 stack 이 react|vue|node|go 인 것만 매칭 — forgen 자체 commands 보존
@@ -237,6 +484,27 @@ function installDevGuideSkillsToCodex(opts) {
237
484
  return { devGuideSkillsPath: codexSkillsDir, devGuideSkillsInstalled: installed, devGuideSkillsRemoved: removed };
238
485
  }
239
486
  // ── P3-3: Codex skills install ────────────────────────────────────────
487
+ /** ADR-014 D3 — Codex 스킬에는 `$ARGUMENTS` 치환 변수가 없다. */
488
+ export function adaptSkillBodyForCodex(body) {
489
+ return body
490
+ .replace(/\{\$ARGUMENTS\}/g, '{the user\'s request text}')
491
+ .replace(/`\$ARGUMENTS`/g, 'the user\'s request text (what follows the skill name)')
492
+ .replace(/\$ARGUMENTS/g, 'the user\'s request text (what follows the skill name)');
493
+ }
494
+ /** ADR-014 D2/D3 — 스킬 본문의 ch-* 에이전트 참조가 Codex 에서 어떻게 해석되는지 명시. */
495
+ const CODEX_SKILL_HOST_NOTE = `
496
+ ---
497
+
498
+ ## Codex host note (forgen-managed)
499
+
500
+ - Sub-agents named \`ch-*\` (ch-planner, ch-executor, ch-verifier, ch-critic, ...) are installed as Codex
501
+ custom agents under \`$CODEX_HOME/agents/ch-*.toml\`. Spawn them with Codex's multi-agent tools when
502
+ available (\`[features] multi_agent = true\` in config.toml).
503
+ - If spawning is unavailable, call the \`invoke-agent\` tool on the \`forgen-compound\` MCP server
504
+ (agent_name + task) or perform that stage inline yourself. Do not skip the stage.
505
+ - The forgen personalized rules for this session arrive as a \`<forgen-rules host="codex">\` block at
506
+ session start. Treat them exactly like Claude Code's .claude/rules/.
507
+ `;
240
508
  function installCodexSkills(opts) {
241
509
  const { sourceDir, targetDir, dryRun } = opts;
242
510
  if (!fs.existsSync(sourceDir))
@@ -264,8 +532,8 @@ function installCodexSkills(opts) {
264
532
  const descMatch = raw.match(/description:\s*(.+)/);
265
533
  const desc = descMatch?.[1]?.trim() ?? skillName;
266
534
  const bodyMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
267
- const body = bodyMatch?.[1]?.trim() ?? raw;
268
- const out = `---\nname: ${skillName}\ndescription: ${desc}\n---\n\n${FORGEN_SKILL_MARKER}\n\n${body}\n`;
535
+ const body = adaptSkillBodyForCodex(bodyMatch?.[1]?.trim() ?? raw);
536
+ const out = `---\nname: ${skillName}\ndescription: ${desc}\n---\n\n${FORGEN_SKILL_MARKER}\n\n${body}\n${CODEX_SKILL_HOST_NOTE}`;
269
537
  fs.mkdirSync(skillDir, { recursive: true });
270
538
  fs.writeFileSync(skillFile, out);
271
539
  count += 1;
@@ -273,7 +541,7 @@ function installCodexSkills(opts) {
273
541
  return { installed: count };
274
542
  }
275
543
  // ── P3-3: AGENTS.md inject ────────────────────────────────────────────
276
- function resolveAgentsMdPath(_pkgRoot) {
544
+ export function resolveAgentsMdPath(_pkgRoot) {
277
545
  // Phase 3 critic fix: pkgRoot 기반 walk-up 은 `npm install -g` 시 시스템 디렉토리
278
546
  // (예: /usr/local/lib/node_modules/forgen) 에 fallback AGENTS.md 작성 위험.
279
547
  // *cwd 기반* 으로 변경 — 사용자 작업 디렉토리의 git root, 없으면 cwd 자체.
@@ -297,7 +565,7 @@ function resolveAgentsMdPath(_pkgRoot) {
297
565
  return path.join(cwd, 'AGENTS.md');
298
566
  }
299
567
  function buildForgenRulesBlock(pkgRoot) {
300
- // forgen 의 핵심 규칙 + 사용자 profile 안내 (가벼운 헤더만 — 실 rule 은 hook chain 이 inject)
568
+ // forgen 의 핵심 규칙 + 사용자 profile 안내 (가벼운 헤더만 — 실 rule 본문은 ADR-014 D1 의 SessionStart hook 주입)
301
569
  const lines = [
302
570
  AGENTS_MD_BEGIN,
303
571
  '## forgen managed rules',
@@ -307,13 +575,13 @@ function buildForgenRulesBlock(pkgRoot) {
307
575
  '- forgen-compound MCP 가 ~/.codex/config.toml 에 등록됨. 학습된 솔루션을 `compound-search` 로 조회 가능.',
308
576
  '- 사용자 교정은 `correction-record` MCP 도구로 즉시 박제 (kind: fix-now / prefer-from-now / avoid-this).',
309
577
  '- forgen 의 4축 profile (quality_safety / autonomy / judgment_philosophy / communication_style) 이 응답 톤 + 검증 깊이를 가이드.',
310
- '- 본 rule 은 cwd 의 AGENTS.md 가 자동 read 되는 Codex 의 user_instructions 경로로 흘러들어감.',
578
+ '- 개인화 룰 본문은 세션 시작 시 SessionStart hook 이 `<forgen-rules host="codex">` 블록으로 주입 (ADR-014). 서브에이전트는 ~/.codex/agents/ch-*.toml.',
311
579
  `- pkgRoot: ${pkgRoot}`,
312
580
  AGENTS_MD_END,
313
581
  ];
314
582
  return lines.join('\n');
315
583
  }
316
- function upsertForgenRulesInAgentsMd(opts) {
584
+ export function upsertForgenRulesInAgentsMd(opts) {
317
585
  const { agentsMdPath, pkgRoot, dryRun } = opts;
318
586
  const block = buildForgenRulesBlock(pkgRoot);
319
587
  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;