@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.
Files changed (139) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +109 -0
  3. package/README.ja.md +4 -4
  4. package/README.md +89 -27
  5. package/README.zh.md +4 -4
  6. package/assets/claude/commands/forge-loop.md +7 -1
  7. package/assets/claude/commands/ship.md +18 -1
  8. package/assets/opencode/forgen.ts +52 -0
  9. package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
  10. package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
  11. package/dist/checks/_shared/model-profile.d.ts +25 -0
  12. package/dist/checks/_shared/model-profile.js +63 -0
  13. package/dist/cli.js +99 -153
  14. package/dist/core/auto-compound-runner.d.ts +0 -11
  15. package/dist/core/auto-compound-runner.js +127 -79
  16. package/dist/core/compound-consent.d.ts +15 -0
  17. package/dist/core/compound-consent.js +48 -0
  18. package/dist/core/compound-sweep-cli.d.ts +57 -0
  19. package/dist/core/compound-sweep-cli.js +354 -0
  20. package/dist/core/config-injector.d.ts +16 -1
  21. package/dist/core/config-injector.js +47 -28
  22. package/dist/core/dashboard-cli.js +40 -16
  23. package/dist/core/dashboard.d.ts +3 -4
  24. package/dist/core/dashboard.js +15 -34
  25. package/dist/core/dev-cli.d.ts +13 -0
  26. package/dist/core/dev-cli.js +70 -0
  27. package/dist/core/doctor.d.ts +16 -5
  28. package/dist/core/doctor.js +161 -176
  29. package/dist/core/drift-score.d.ts +2 -0
  30. package/dist/core/drift-score.js +9 -1
  31. package/dist/core/harness.js +115 -43
  32. package/dist/core/health-cli.d.ts +2 -0
  33. package/dist/core/health-cli.js +6 -1
  34. package/dist/core/host-detect.d.ts +3 -1
  35. package/dist/core/host-detect.js +26 -1
  36. package/dist/core/migrate-cli.js +15 -0
  37. package/dist/core/migrate-evidence-host.d.ts +2 -1
  38. package/dist/core/migrate-tenetx.d.ts +50 -0
  39. package/dist/core/migrate-tenetx.js +262 -0
  40. package/dist/core/probe-workflow-cli.d.ts +3 -3
  41. package/dist/core/probe-workflow-cli.js +13 -13
  42. package/dist/core/recall-cli.js +1 -1
  43. package/dist/core/regress-map-cli.js +1 -1
  44. package/dist/core/rendered-rules-manifest.d.ts +29 -0
  45. package/dist/core/rendered-rules-manifest.js +60 -0
  46. package/dist/core/session-store.js +14 -3
  47. package/dist/core/settings-injector.d.ts +3 -0
  48. package/dist/core/settings-injector.js +12 -16
  49. package/dist/core/spawn.d.ts +11 -1
  50. package/dist/core/spawn.js +79 -7
  51. package/dist/core/state-gc.js +1 -0
  52. package/dist/core/status-cli.d.ts +20 -0
  53. package/dist/core/status-cli.js +100 -0
  54. package/dist/core/statusline-cli.d.ts +7 -0
  55. package/dist/core/statusline-cli.js +63 -19
  56. package/dist/core/transcript-summary.d.ts +18 -0
  57. package/dist/core/transcript-summary.js +81 -0
  58. package/dist/core/trust-layer-intent.d.ts +21 -1
  59. package/dist/core/trust-layer-intent.js +7 -0
  60. package/dist/core/types.d.ts +3 -2
  61. package/dist/core/uninstall.js +12 -0
  62. package/dist/core/usage-telemetry.d.ts +7 -1
  63. package/dist/core/usage-telemetry.js +7 -9
  64. package/dist/core/v1-bootstrap.js +1 -1
  65. package/dist/core/watch-cli.js +1 -1
  66. package/dist/engine/compound-extractor.js +10 -0
  67. package/dist/engine/compound-loop.js +35 -6
  68. package/dist/engine/compound-share.d.ts +85 -0
  69. package/dist/engine/compound-share.js +606 -0
  70. package/dist/engine/correction-cluster-runner.d.ts +38 -0
  71. package/dist/engine/correction-cluster-runner.js +188 -0
  72. package/dist/engine/correction-clustering.d.ts +80 -0
  73. package/dist/engine/correction-clustering.js +167 -0
  74. package/dist/engine/enforce-classifier.d.ts +10 -1
  75. package/dist/engine/enforce-classifier.js +111 -22
  76. package/dist/engine/extraction-session.js +10 -3
  77. package/dist/engine/private-filter.d.ts +36 -0
  78. package/dist/engine/private-filter.js +100 -0
  79. package/dist/engine/ranking-pipeline.js +4 -2
  80. package/dist/engine/relevance-gate.d.ts +12 -0
  81. package/dist/engine/relevance-gate.js +12 -0
  82. package/dist/engine/roi-demotion.d.ts +79 -0
  83. package/dist/engine/roi-demotion.js +159 -0
  84. package/dist/engine/solution-format.d.ts +1 -0
  85. package/dist/engine/solution-format.js +26 -0
  86. package/dist/engine/solution-matcher.js +10 -1
  87. package/dist/fgx.js +19 -6
  88. package/dist/forge/cli.js +8 -2
  89. package/dist/hooks/compound-reflection.js +6 -1
  90. package/dist/hooks/context-guard.d.ts +16 -1
  91. package/dist/hooks/context-guard.js +92 -46
  92. package/dist/hooks/post-tool-use.js +2 -3
  93. package/dist/hooks/pre-compact.js +14 -0
  94. package/dist/hooks/pre-tool-use.js +5 -1
  95. package/dist/hooks/shared/stop-triggers.d.ts +29 -2
  96. package/dist/hooks/shared/stop-triggers.js +35 -2
  97. package/dist/hooks/solution-injector.d.ts +28 -0
  98. package/dist/hooks/solution-injector.js +126 -28
  99. package/dist/hooks/stop-guard.js +5 -2
  100. package/dist/hooks/subagent-stop-guard.js +4 -1
  101. package/dist/host/capabilities-claude.js +1 -0
  102. package/dist/host/capabilities-codex.js +1 -0
  103. package/dist/host/capabilities-opencode.d.ts +26 -0
  104. package/dist/host/capabilities-opencode.js +78 -0
  105. package/dist/host/capabilities-registry.d.ts +7 -0
  106. package/dist/host/capabilities-registry.js +14 -0
  107. package/dist/host/exec-host.d.ts +4 -3
  108. package/dist/host/exec-host.js +2 -0
  109. package/dist/host/host-binding.d.ts +27 -0
  110. package/dist/host/host-binding.js +11 -0
  111. package/dist/host/host-runtime.js +18 -0
  112. package/dist/host/install-codex.d.ts +8 -0
  113. package/dist/host/install-codex.js +2 -2
  114. package/dist/host/install-opencode.d.ts +38 -0
  115. package/dist/host/install-opencode.js +148 -0
  116. package/dist/host/install-orchestrator.d.ts +4 -1
  117. package/dist/host/install-orchestrator.js +20 -1
  118. package/dist/host/invoke-agent.d.ts +3 -2
  119. package/dist/host/opencode/context-cli.d.ts +15 -0
  120. package/dist/host/opencode/context-cli.js +25 -0
  121. package/dist/host/opencode/guard-cli.d.ts +13 -0
  122. package/dist/host/opencode/guard-cli.js +39 -0
  123. package/dist/host/opencode/plugin/forgen.d.ts +31 -0
  124. package/dist/host/opencode/plugin/forgen.js +60 -0
  125. package/dist/host/opencode/translate.d.ts +49 -0
  126. package/dist/host/opencode/translate.js +96 -0
  127. package/dist/host/parity-harness.d.ts +10 -2
  128. package/dist/host/projection.js +11 -0
  129. package/dist/mcp/tools.js +17 -4
  130. package/dist/store/evidence-store.d.ts +2 -6
  131. package/dist/store/evidence-store.js +68 -14
  132. package/dist/store/host-mismatch.d.ts +2 -1
  133. package/dist/store/host-mismatch.js +8 -8
  134. package/dist/store/profile-store.d.ts +3 -2
  135. package/dist/store/types.d.ts +9 -2
  136. package/package.json +3 -2
  137. package/plugin.json +2 -2
  138. package/skills/forge-loop/SKILL.md +7 -1
  139. package/skills/ship/SKILL.md +18 -1
@@ -0,0 +1,148 @@
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
+ import * as fs from 'node:fs';
13
+ import * as os from 'node:os';
14
+ import * as path from 'node:path';
15
+ import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser';
16
+ import { createLogger } from '../core/logger.js';
17
+ import { resolveAgentsMdPath, upsertForgenRulesInAgentsMd } from './install-codex.js';
18
+ const log = createLogger('install-opencode');
19
+ const PLUGIN_FILENAME = 'forgen.ts';
20
+ const PLUGIN_MARKER = 'forgen-managed';
21
+ const MCP_SERVER_NAME = 'forgen-compound';
22
+ export function resolveOpencodeConfigDir(opts) {
23
+ if (opts.opencodeConfigDir)
24
+ return opts.opencodeConfigDir;
25
+ // OpenCode 는 XDG_CONFIG_HOME 을 존중한다(config docs). forgen 도 동일 해석해야 실제 로드
26
+ // 위치에 배포된다 — hardcode ~/.config 이면 XDG override 사용자에게 미도달.
27
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
28
+ const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), '.config');
29
+ return path.join(base, 'opencode');
30
+ }
31
+ /** 기존 config 파일 경로 감지 — opencode.jsonc 우선, 없으면 opencode.json (둘 다 없으면 .json 신규). */
32
+ function resolveConfigFilePath(configDir) {
33
+ const jsonc = path.join(configDir, 'opencode.jsonc');
34
+ if (fs.existsSync(jsonc))
35
+ return jsonc;
36
+ return path.join(configDir, 'opencode.json');
37
+ }
38
+ /**
39
+ * config 텍스트에 mcp.forgen-compound 를 surgical 병합. jsonc-parser 로 주석/포맷 보존.
40
+ * 파싱 불가(errors)면 clobber 하지 않고 null 반환(데이터 손실 방지, 리뷰 HIGH).
41
+ */
42
+ function upsertOpencodeMcp(currentText, pkgRoot) {
43
+ const serverPath = path.join(pkgRoot, 'dist', 'mcp', 'server.js');
44
+ const desired = { type: 'local', command: ['node', serverPath, '--host=opencode'], enabled: true };
45
+ const src = currentText.trim().length > 0 ? currentText : '{}';
46
+ // 파싱 검증 — 유효 JSONC 가 아니면(구조 손상) abort.
47
+ const errors = [];
48
+ const parsed = parseJsonc(src, errors, { allowTrailingComma: true });
49
+ if (errors.length > 0 || parsed === undefined || (parsed !== null && typeof parsed !== 'object')) {
50
+ return { content: null, alreadyPresent: false, unparseable: true };
51
+ }
52
+ const existing = parsed?.mcp?.[MCP_SERVER_NAME];
53
+ const alreadyPresent = JSON.stringify(existing) === JSON.stringify(desired);
54
+ if (alreadyPresent)
55
+ return { content: src, alreadyPresent: true, unparseable: false };
56
+ // surgical edit — 주석/기타 키/포맷 보존.
57
+ let out = src;
58
+ const opts = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
59
+ out = applyEdits(out, modify(out, ['mcp', MCP_SERVER_NAME], desired, opts));
60
+ // $schema 없으면 추가(있으면 유지).
61
+ if (parsed.$schema === undefined) {
62
+ out = applyEdits(out, modify(out, ['$schema'], 'https://opencode.ai/config.json', opts));
63
+ }
64
+ return { content: out.endsWith('\n') ? out : `${out}\n`, alreadyPresent: false, unparseable: false };
65
+ }
66
+ /** 배포 plugin 소스 로드 + GUARD_CMD 를 절대 forgen CLI 경로로 치환(런타임 PATH 비의존). */
67
+ function loadPluginSource(pkgRoot) {
68
+ const srcPath = path.join(pkgRoot, 'assets', 'opencode', PLUGIN_FILENAME);
69
+ let src;
70
+ try {
71
+ src = fs.readFileSync(srcPath, 'utf-8');
72
+ }
73
+ catch (e) {
74
+ log.debug('opencode plugin asset 읽기 실패', e);
75
+ return null;
76
+ }
77
+ const cliPath = path.join(pkgRoot, 'dist', 'cli.js');
78
+ // ["forgen", "<sub>"] → ["node", "<abs cli>", "<sub>"] (PATH 비의존, 리뷰 MED4).
79
+ const replaced = src
80
+ .replace(/\["forgen",\s*"opencode-guard"\]/, `["node", ${JSON.stringify(cliPath)}, "opencode-guard"]`)
81
+ .replace(/\["forgen",\s*"opencode-context"\]/, `["node", ${JSON.stringify(cliPath)}, "opencode-context"]`);
82
+ return replaced;
83
+ }
84
+ export function planOpencodeInstall(opts) {
85
+ const configDir = resolveOpencodeConfigDir(opts);
86
+ const pluginsDir = path.join(configDir, 'plugins');
87
+ const pluginPath = path.join(pluginsDir, PLUGIN_FILENAME);
88
+ const mcpConfigPath = resolveConfigFilePath(configDir);
89
+ const registerMcp = opts.registerMcp ?? true;
90
+ const dryRun = opts.dryRun ?? false;
91
+ const result = {
92
+ configDir,
93
+ pluginPath,
94
+ pluginInstalled: false,
95
+ mcpRegistered: false,
96
+ mcpAlreadyPresent: false,
97
+ mcpConfigPath,
98
+ mcpSkippedUnparseable: false,
99
+ agentsMdInjected: false,
100
+ };
101
+ // ── 1) plugin 배포 (marker 확인 + 사용자 파일 백업) ──
102
+ const pluginSrc = loadPluginSource(opts.pkgRoot);
103
+ if (pluginSrc) {
104
+ // 기존 plugin 이 forgen-managed 가 아니면(사용자 소유) 백업.
105
+ if (!dryRun && fs.existsSync(pluginPath)) {
106
+ const existing = fs.readFileSync(pluginPath, 'utf-8');
107
+ if (!existing.includes(PLUGIN_MARKER)) {
108
+ const bak = `${pluginPath}.bak`;
109
+ fs.copyFileSync(pluginPath, bak);
110
+ result.pluginBackupPath = bak;
111
+ }
112
+ }
113
+ if (!dryRun) {
114
+ fs.mkdirSync(pluginsDir, { recursive: true });
115
+ fs.writeFileSync(pluginPath, pluginSrc, 'utf-8');
116
+ }
117
+ result.pluginInstalled = true;
118
+ }
119
+ // ── 2) MCP surgical 병합 (JSONC 안전 + 백업 + abort-on-unparseable) ──
120
+ if (registerMcp) {
121
+ const current = fs.existsSync(mcpConfigPath) ? fs.readFileSync(mcpConfigPath, 'utf-8') : '';
122
+ const merge = upsertOpencodeMcp(current, opts.pkgRoot);
123
+ if (merge.unparseable) {
124
+ // 파싱 불가 → 사용자 config 보존, MCP 스킵 + 경고 (데이터 손실 방지).
125
+ result.mcpSkippedUnparseable = true;
126
+ log.debug(`opencode config 파싱 불가 — MCP 등록 스킵(사용자 파일 보존): ${mcpConfigPath}`);
127
+ }
128
+ else {
129
+ result.mcpAlreadyPresent = merge.alreadyPresent;
130
+ result.mcpRegistered = !merge.alreadyPresent;
131
+ if (!dryRun && merge.content !== null && !merge.alreadyPresent) {
132
+ fs.mkdirSync(configDir, { recursive: true });
133
+ // 쓰기 전 백업(기존 파일이 있을 때만).
134
+ if (current.length > 0) {
135
+ const bak = `${mcpConfigPath}.bak`;
136
+ fs.writeFileSync(bak, current, 'utf-8');
137
+ result.mcpBackupPath = bak;
138
+ }
139
+ fs.writeFileSync(mcpConfigPath, merge.content, 'utf-8');
140
+ }
141
+ }
142
+ }
143
+ // ── 3) AGENTS.md rules (Codex 헬퍼 재사용) ──
144
+ const agentsMdPath = opts.agentsMdPath ?? resolveAgentsMdPath(opts.pkgRoot);
145
+ const agentsResult = upsertForgenRulesInAgentsMd({ agentsMdPath, pkgRoot: opts.pkgRoot, dryRun });
146
+ result.agentsMdInjected = agentsResult.injected;
147
+ return result;
148
+ }
@@ -13,7 +13,9 @@
13
13
  import { detectAvailableHosts } from '../core/host-detect.js';
14
14
  import { type ClaudeInstallResult } from './install-claude.js';
15
15
  import { type CodexInstallResult } from './install-codex.js';
16
- export type InstallTarget = 'claude' | 'codex' | 'both';
16
+ import { type OpencodeInstallResult } from './install-opencode.js';
17
+ import type { HostId } from '../core/trust-layer-intent.js';
18
+ export type InstallTarget = HostId | 'both';
17
19
  export interface OrchestratorOptions {
18
20
  /** Sub-command 인자: 'claude'|'codex'|'both' 또는 undefined (interactive). */
19
21
  target?: string;
@@ -25,6 +27,7 @@ export interface OrchestratorResult {
25
27
  target: InstallTarget;
26
28
  claude?: ClaudeInstallResult;
27
29
  codex?: CodexInstallResult;
30
+ opencode?: OpencodeInstallResult;
28
31
  detection: ReturnType<typeof detectAvailableHosts>;
29
32
  }
30
33
  export declare function runInstall(opts: OrchestratorOptions): Promise<OrchestratorResult | null>;
@@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url';
16
16
  import { detectAvailableHosts } from '../core/host-detect.js';
17
17
  import { planClaudeInstall } from './install-claude.js';
18
18
  import { planCodexInstall } from './install-codex.js';
19
+ import { planOpencodeInstall } from './install-opencode.js';
19
20
  function askChoice(rl, question, validChoices) {
20
21
  return new Promise((resolve) => {
21
22
  const ask = () => {
@@ -49,6 +50,11 @@ async function chooseTargetInteractively(detection) {
49
50
  console.log(' Detected hosts:');
50
51
  console.log(renderHostStatus(detection.claude));
51
52
  console.log(renderHostStatus(detection.codex));
53
+ // W3-3 P1: OpenCode 는 감지만 하고 설치 대상엔 아직 안 넣는다(plugin 슬림 미구현).
54
+ // 감지됐을 때만 정직하게 "detected, install pending" 안내.
55
+ if (detection.opencode.available) {
56
+ console.log(` ⋯ opencode (detected — 설치는 P1 plugin 슬림 착지 후 지원 예정, 현재 미지원)`);
57
+ }
52
58
  console.log('');
53
59
  if (detection.noneAvailable) {
54
60
  console.log(' ⚠ Neither Claude nor Codex detected. Install one of:');
@@ -75,7 +81,7 @@ async function chooseTargetInteractively(detection) {
75
81
  export async function runInstall(opts) {
76
82
  const detection = detectAvailableHosts();
77
83
  let target;
78
- if (opts.target === 'claude' || opts.target === 'codex' || opts.target === 'both') {
84
+ if (opts.target === 'claude' || opts.target === 'codex' || opts.target === 'opencode' || opts.target === 'both') {
79
85
  target = opts.target;
80
86
  }
81
87
  else if (opts.target === undefined) {
@@ -96,6 +102,10 @@ export async function runInstall(opts) {
96
102
  if (target === 'codex' || target === 'both') {
97
103
  result.codex = planCodexInstall({ pkgRoot: opts.pkgRoot, dryRun, registerMcp });
98
104
  }
105
+ // W3-3: opencode 는 명시 타겟일 때만 설치('both' 는 claude+codex primary-pair 유지).
106
+ if (target === 'opencode') {
107
+ result.opencode = planOpencodeInstall({ pkgRoot: opts.pkgRoot, dryRun, registerMcp });
108
+ }
99
109
  return result;
100
110
  }
101
111
  /** CLI 출력 포맷터 — orchestrator 결과를 사용자에게 표시. */
@@ -118,6 +128,15 @@ export function renderResult(result, dryRun) {
118
128
  lines.push(` hooks.json: ${result.codex.hooksCount} forgen hooks (preserved user: ${result.codex.preservedUserHookCount})`);
119
129
  lines.push(` MCP: ${result.codex.mcpAlreadyPresent ? 'already present' : (result.codex.mcpRegistered ? 'registered' : 'skipped')}`);
120
130
  }
131
+ if (result.opencode) {
132
+ lines.push('');
133
+ lines.push(' OpenCode (P1 — block-tool-use + MCP, 실험적):');
134
+ lines.push(` config dir: ${result.opencode.configDir}`);
135
+ lines.push(` plugin: ${result.opencode.pluginInstalled ? 'installed' : 'skipped'} → ${result.opencode.pluginPath}`);
136
+ lines.push(` MCP: ${result.opencode.mcpAlreadyPresent ? 'already present' : (result.opencode.mcpRegistered ? 'registered' : 'skipped')}`);
137
+ lines.push(` AGENTS.md rules: ${result.opencode.agentsMdInjected ? 'injected' : 'skipped'}`);
138
+ lines.push(' note: 완료가드/context 주입은 후속 증분 (projection/inject 미완).');
139
+ }
121
140
  lines.push('');
122
141
  return lines.join('\n');
123
142
  }
@@ -9,17 +9,18 @@
9
9
  * 또 sub-agent 호출 시도 → 차단).
10
10
  */
11
11
  import { type ExecHostResult } from './exec-host.js';
12
+ import type { HostId } from '../core/trust-layer-intent.js';
12
13
  export interface InvokeAgentOptions {
13
14
  agentName: string;
14
15
  task: string;
15
16
  /** Child process timeout (ms). Default 60s. */
16
17
  timeoutMs?: number;
17
18
  /** Override host (default: profile.default_host). */
18
- host?: 'claude' | 'codex';
19
+ host?: HostId;
19
20
  }
20
21
  export interface InvokeAgentResult {
21
22
  agentName: string;
22
- host: 'claude' | 'codex';
23
+ host: HostId;
23
24
  summary: string;
24
25
  durationMs: number;
25
26
  usage: ExecHostResult['usage'];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `forgen opencode-context` — OpenCode plugin 이 compaction 시 주입할 forgen context 출력 (W3-3 P1).
3
+ *
4
+ * OpenCode 의 유일한 문서화된 context 주입 표면은 `experimental.session.compacting`
5
+ * (output.context.push) — *compaction 시점*에 context 를 얹는다. forgen 은 여기에 활성
6
+ * forge-loop 상태(≤1KB)를 얹어 **compaction 을 넘어 목표가 유지**되게 한다
7
+ * (forge-loop-state-inject intent, capabilities-opencode=partial).
8
+ *
9
+ * 정직한 스코프: Claude 의 per-prompt 주입(UserPromptSubmit)과 달리 OpenCode 플러그인은
10
+ * 매 프롬프트 주입 표면이 문서화돼 있지 않다 → 정적 룰은 AGENTS.md(install), compound 는
11
+ * MCP tool 로 커버하고, 동적 forge-loop 상태만 compaction 에 얹는다.
12
+ *
13
+ * 활성 forge-loop 이 없으면 빈 출력(주입할 것 없음).
14
+ */
15
+ export declare function runOpencodeContext(): void;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `forgen opencode-context` — OpenCode plugin 이 compaction 시 주입할 forgen context 출력 (W3-3 P1).
3
+ *
4
+ * OpenCode 의 유일한 문서화된 context 주입 표면은 `experimental.session.compacting`
5
+ * (output.context.push) — *compaction 시점*에 context 를 얹는다. forgen 은 여기에 활성
6
+ * forge-loop 상태(≤1KB)를 얹어 **compaction 을 넘어 목표가 유지**되게 한다
7
+ * (forge-loop-state-inject intent, capabilities-opencode=partial).
8
+ *
9
+ * 정직한 스코프: Claude 의 per-prompt 주입(UserPromptSubmit)과 달리 OpenCode 플러그인은
10
+ * 매 프롬프트 주입 표면이 문서화돼 있지 않다 → 정적 룰은 AGENTS.md(install), compound 는
11
+ * MCP tool 로 커버하고, 동적 forge-loop 상태만 compaction 에 얹는다.
12
+ *
13
+ * 활성 forge-loop 이 없으면 빈 출력(주입할 것 없음).
14
+ */
15
+ import { readForgeLoopState, renderForgeLoopForSession } from '../../hooks/shared/forge-loop-state.js';
16
+ export function runOpencodeContext() {
17
+ let block = null;
18
+ try {
19
+ block = renderForgeLoopForSession(readForgeLoopState());
20
+ }
21
+ catch {
22
+ block = null; // fail-safe — 주입 실패가 세션을 막지 않는다
23
+ }
24
+ process.stdout.write(block ?? '');
25
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `forgen opencode-guard` — OpenCode plugin 슬림이 호출하는 경량 브릿지 CLI (W3-3 P1).
3
+ *
4
+ * OpenCode 플러그인(`~/.config/opencode/plugins/forgen.ts`)이 `tool.execute.before` 에서
5
+ * `{tool, args}` 를 stdin 으로 넘기면, 이 명령이 forgen PreToolUse 가드 체인을 돌려
6
+ * `{block, reason}` 을 stdout 으로 반환한다. 플러그인은 block 이면 throw(도구 차단).
7
+ *
8
+ * 플러그인을 얇게 유지(= 번역/가드 로직 중복 없음, drift-free)하는 대신 forgen 이 이
9
+ * 한 명령으로 전부 처리한다. per-tool-call 이라 cli.ts 최상단 fast-path 로 진입한다.
10
+ *
11
+ * fail-open: stdin 파싱 실패/가드 오류는 도구를 막지 않는다 → {block:false}.
12
+ */
13
+ export declare function runOpencodeGuard(): Promise<void>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `forgen opencode-guard` — OpenCode plugin 슬림이 호출하는 경량 브릿지 CLI (W3-3 P1).
3
+ *
4
+ * OpenCode 플러그인(`~/.config/opencode/plugins/forgen.ts`)이 `tool.execute.before` 에서
5
+ * `{tool, args}` 를 stdin 으로 넘기면, 이 명령이 forgen PreToolUse 가드 체인을 돌려
6
+ * `{block, reason}` 을 stdout 으로 반환한다. 플러그인은 block 이면 throw(도구 차단).
7
+ *
8
+ * 플러그인을 얇게 유지(= 번역/가드 로직 중복 없음, drift-free)하는 대신 forgen 이 이
9
+ * 한 명령으로 전부 처리한다. per-tool-call 이라 cli.ts 최상단 fast-path 로 진입한다.
10
+ *
11
+ * fail-open: stdin 파싱 실패/가드 오류는 도구를 막지 않는다 → {block:false}.
12
+ */
13
+ import * as path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { toolBeforeToClaudeInput } from './translate.js';
16
+ import { runPreToolGuards } from './plugin/forgen.js';
17
+ /** 이 CLI(dist/host/opencode/guard-cli.js) 기준 forgen dist/hooks 절대경로. */
18
+ function forgenHookDir() {
19
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'hooks');
20
+ }
21
+ async function readStdin() {
22
+ const chunks = [];
23
+ for await (const chunk of process.stdin)
24
+ chunks.push(chunk);
25
+ return Buffer.concat(chunks).toString('utf-8');
26
+ }
27
+ export async function runOpencodeGuard() {
28
+ let payload = {};
29
+ try {
30
+ payload = JSON.parse((await readStdin()) || '{}');
31
+ }
32
+ catch {
33
+ process.stdout.write(JSON.stringify({ block: false }));
34
+ return;
35
+ }
36
+ const claudeInput = toolBeforeToClaudeInput(payload.tool ?? '', payload.args);
37
+ const decision = await runPreToolGuards(claudeInput, { hookDir: forgenHookDir() });
38
+ process.stdout.write(JSON.stringify(decision));
39
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * forgen OpenCode 가드 러너 (W3-3 P1, adapter-plan §4.1 in-process-plugin binding).
3
+ *
4
+ * OpenCode `tool.execute.before` → forgen 기존 PreToolUse 가드(pre-tool-use/db-guard) 브릿지의
5
+ * *핵심 로직*. `forgen opencode-guard` CLI(guard-cli.ts)가 이 함수를 호출한다. 실 배포되는
6
+ * 플러그인(assets/opencode/forgen.ts)은 이 CLI 를 async 로 부르는 얇은 shim 이라, OpenCode
7
+ * 이벤트루프를 막지 않는다.
8
+ *
9
+ * 리뷰 MED (a): spawnSync 는 in-process 플러그인에서 이벤트루프를 막으므로 **async spawn**
10
+ * (execFile+await)으로 구현. (b): spawn 실패는 fail-open 하되 **로그**를 남겨 systematically
11
+ * -broken 가드를 관찰 가능하게 한다.
12
+ *
13
+ * fail-open 정책: 가드 해소/spawn 실패는 도구를 막지 않는다(forgen hook 실패 정책과 동일).
14
+ */
15
+ import { type ClaudePreToolInput, type OpencodeToolDecision } from '../translate.js';
16
+ /** PreToolUse 가드 순서: pre-tool-use(룰+위험명령 디스패처) → db-guard(rm -rf/위험 SQL). */
17
+ export declare const PRE_TOOL_GUARDS: readonly ["pre-tool-use.js", "db-guard.js"];
18
+ export interface GuardRunOptions {
19
+ /** forgen hook 바이너리 디렉터리. guard-cli 가 forgen dist/hooks 를 주입. */
20
+ hookDir?: string;
21
+ /** spawn 타임아웃(ms). */
22
+ timeoutMs?: number;
23
+ }
24
+ /** forgen hook 디렉터리 해소: 명시 > FORGEN_HOOK_DIR env > ~/.forgen/hooks 폴백. */
25
+ export declare function resolveHookDir(explicit?: string): string;
26
+ /**
27
+ * Claude PreToolUse 입력을 forgen 가드 체인에 흘려 첫 deny 를 반환(테스트 가능한 핵심).
28
+ * **async** — execFile 로 비동기 spawn (이벤트루프 비차단). 어느 가드든 deny 하면 즉시 block.
29
+ * 전부 통과/실패(fail-open)면 block=false. spawn 실패는 로그 후 계속(fail-open).
30
+ */
31
+ export declare function runPreToolGuards(claudeInput: ClaudePreToolInput, opts?: GuardRunOptions): Promise<OpencodeToolDecision>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * forgen OpenCode 가드 러너 (W3-3 P1, adapter-plan §4.1 in-process-plugin binding).
3
+ *
4
+ * OpenCode `tool.execute.before` → forgen 기존 PreToolUse 가드(pre-tool-use/db-guard) 브릿지의
5
+ * *핵심 로직*. `forgen opencode-guard` CLI(guard-cli.ts)가 이 함수를 호출한다. 실 배포되는
6
+ * 플러그인(assets/opencode/forgen.ts)은 이 CLI 를 async 로 부르는 얇은 shim 이라, OpenCode
7
+ * 이벤트루프를 막지 않는다.
8
+ *
9
+ * 리뷰 MED (a): spawnSync 는 in-process 플러그인에서 이벤트루프를 막으므로 **async spawn**
10
+ * (execFile+await)으로 구현. (b): spawn 실패는 fail-open 하되 **로그**를 남겨 systematically
11
+ * -broken 가드를 관찰 가능하게 한다.
12
+ *
13
+ * fail-open 정책: 가드 해소/spawn 실패는 도구를 막지 않는다(forgen hook 실패 정책과 동일).
14
+ */
15
+ import { execFile } from 'node:child_process';
16
+ import * as os from 'node:os';
17
+ import * as path from 'node:path';
18
+ import { promisify } from 'node:util';
19
+ import { createLogger } from '../../../core/logger.js';
20
+ import { decisionFromForgenOutput, } from '../translate.js';
21
+ const execFileAsync = promisify(execFile);
22
+ const log = createLogger('opencode-guard');
23
+ /** PreToolUse 가드 순서: pre-tool-use(룰+위험명령 디스패처) → db-guard(rm -rf/위험 SQL). */
24
+ export const PRE_TOOL_GUARDS = ['pre-tool-use.js', 'db-guard.js'];
25
+ /** forgen hook 디렉터리 해소: 명시 > FORGEN_HOOK_DIR env > ~/.forgen/hooks 폴백. */
26
+ export function resolveHookDir(explicit) {
27
+ return explicit ?? process.env.FORGEN_HOOK_DIR ?? path.join(os.homedir(), '.forgen', 'hooks');
28
+ }
29
+ /**
30
+ * Claude PreToolUse 입력을 forgen 가드 체인에 흘려 첫 deny 를 반환(테스트 가능한 핵심).
31
+ * **async** — execFile 로 비동기 spawn (이벤트루프 비차단). 어느 가드든 deny 하면 즉시 block.
32
+ * 전부 통과/실패(fail-open)면 block=false. spawn 실패는 로그 후 계속(fail-open).
33
+ */
34
+ export async function runPreToolGuards(claudeInput, opts = {}) {
35
+ const dir = resolveHookDir(opts.hookDir);
36
+ const stdin = JSON.stringify(claudeInput);
37
+ const timeout = opts.timeoutMs ?? 5000;
38
+ for (const guard of PRE_TOOL_GUARDS) {
39
+ try {
40
+ const child = execFileAsync('node', [path.join(dir, guard)], { timeout, encoding: 'utf-8' });
41
+ child.child.stdin?.end(stdin);
42
+ const { stdout } = await child;
43
+ const decision = decisionFromForgenOutput(stdout ?? '');
44
+ if (decision.block)
45
+ return decision;
46
+ }
47
+ catch (e) {
48
+ // fail-open, 그러나 관찰 가능하게 — systematically-broken 가드 발견용 (리뷰 MED b).
49
+ // execFile 은 non-zero exit(가드가 정상 deny 로 종료해도)에서 throw 할 수 있어 stdout 을 확인.
50
+ const stdout = e?.stdout;
51
+ if (typeof stdout === 'string') {
52
+ const decision = decisionFromForgenOutput(stdout);
53
+ if (decision.block)
54
+ return decision;
55
+ }
56
+ log.debug(`opencode 가드 spawn 실패(fail-open): ${guard}`, e);
57
+ }
58
+ }
59
+ return { block: false };
60
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * OpenCode ↔ Claude hook translation (W3-3 P1 plugin 슬림, adapter-plan §4.1).
3
+ *
4
+ * OpenCode 는 in-process TS plugin(`tool.execute.before(input, output)`)이라 forgen 의
5
+ * subprocess hook(Claude PreToolUse schema stdin/stdout)과 형태가 다르다. 이 모듈은 그 사이
6
+ * *순수* 번역만 담당한다 — spawn/파일 IO 는 plugin shim(forgen.ts)이 수행.
7
+ *
8
+ * 번역 축:
9
+ * 1. tool 이름: opencode(소문자 bash/read/write/edit…) → Claude(Bash/Read/Write/Edit…).
10
+ * 2. tool 인자 키: opencode(filePath) → Claude(file_path) 등, 가드가 읽는 필드 정합.
11
+ * 3. 결정: forgen PreToolUse 출력(permissionDecision:deny) → OpenCode block(throw) 신호.
12
+ *
13
+ * 이 계약은 docs-level(adapter-plan §2.2 web probe) 기반이며, 실제 OpenCode tool 스키마로
14
+ * plugin 슬림 통합 시 검증·보정한다.
15
+ */
16
+ /** Claude PreToolUse hook 에 넣을 입력 (forgen hook 이 stdin 으로 받는 형태). */
17
+ export interface ClaudePreToolInput {
18
+ hook_event_name: 'PreToolUse';
19
+ tool_name: string;
20
+ tool_input: Record<string, unknown>;
21
+ }
22
+ /** forgen PreToolUse hook 결정 → OpenCode 로 번역한 판정. */
23
+ export interface OpencodeToolDecision {
24
+ /** true 면 plugin 이 throw 해서 도구 실행을 차단. */
25
+ block: boolean;
26
+ reason?: string;
27
+ }
28
+ /**
29
+ * OpenCode tool 이름 → Claude tool 이름. forgen 가드(db-guard/pre-tool-use)가 Claude tool
30
+ * 이름 관습(Bash/Read/Write/Edit)으로 매칭하므로 정합이 필요하다.
31
+ */
32
+ export declare const OPENCODE_TO_CLAUDE_TOOL: Readonly<Record<string, string>>;
33
+ /** 매핑에 없으면 첫 글자 대문자화(안전 폴백) — 가드가 못 알아봐도 fail-open(허용). */
34
+ export declare function mapToolName(opencodeTool: string): string;
35
+ /**
36
+ * OpenCode tool 인자 키 → Claude tool_input 키. 가드가 읽는 필드만 정규화한다:
37
+ * - filePath → file_path (Read/Write/Edit 대상 파일)
38
+ * - command 는 그대로 (Bash) — db-guard 가 tool_input.command 로 rm -rf 감지.
39
+ * 나머지 키는 원본 유지(추가 정보는 무해).
40
+ */
41
+ export declare function normalizeToolArgs(args: Record<string, unknown> | undefined | null): Record<string, unknown>;
42
+ /** OpenCode `tool.execute.before` (input.tool, output.args) → Claude PreToolUse stdin. */
43
+ export declare function toolBeforeToClaudeInput(opencodeTool: string, opencodeArgs: Record<string, unknown> | undefined | null): ClaudePreToolInput;
44
+ /**
45
+ * forgen PreToolUse hook 의 stdout(JSON 여러 줄일 수 있음) → OpenCode 판정.
46
+ * permissionDecision:"deny" → block(throw). 그 외/파싱 실패 → 허용(fail-open — 가드 부재가
47
+ * 도구를 막지 않는다, forgen 의 hook 실패 정책과 동일).
48
+ */
49
+ export declare function decisionFromForgenOutput(stdout: string): OpencodeToolDecision;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * OpenCode ↔ Claude hook translation (W3-3 P1 plugin 슬림, adapter-plan §4.1).
3
+ *
4
+ * OpenCode 는 in-process TS plugin(`tool.execute.before(input, output)`)이라 forgen 의
5
+ * subprocess hook(Claude PreToolUse schema stdin/stdout)과 형태가 다르다. 이 모듈은 그 사이
6
+ * *순수* 번역만 담당한다 — spawn/파일 IO 는 plugin shim(forgen.ts)이 수행.
7
+ *
8
+ * 번역 축:
9
+ * 1. tool 이름: opencode(소문자 bash/read/write/edit…) → Claude(Bash/Read/Write/Edit…).
10
+ * 2. tool 인자 키: opencode(filePath) → Claude(file_path) 등, 가드가 읽는 필드 정합.
11
+ * 3. 결정: forgen PreToolUse 출력(permissionDecision:deny) → OpenCode block(throw) 신호.
12
+ *
13
+ * 이 계약은 docs-level(adapter-plan §2.2 web probe) 기반이며, 실제 OpenCode tool 스키마로
14
+ * plugin 슬림 통합 시 검증·보정한다.
15
+ */
16
+ /**
17
+ * OpenCode tool 이름 → Claude tool 이름. forgen 가드(db-guard/pre-tool-use)가 Claude tool
18
+ * 이름 관습(Bash/Read/Write/Edit)으로 매칭하므로 정합이 필요하다.
19
+ */
20
+ export const OPENCODE_TO_CLAUDE_TOOL = Object.freeze({
21
+ bash: 'Bash',
22
+ read: 'Read',
23
+ write: 'Write',
24
+ edit: 'Edit',
25
+ patch: 'Edit',
26
+ grep: 'Grep',
27
+ glob: 'Glob',
28
+ list: 'LS',
29
+ webfetch: 'WebFetch',
30
+ task: 'Task',
31
+ });
32
+ /** 매핑에 없으면 첫 글자 대문자화(안전 폴백) — 가드가 못 알아봐도 fail-open(허용). */
33
+ export function mapToolName(opencodeTool) {
34
+ if (!opencodeTool)
35
+ return '';
36
+ return OPENCODE_TO_CLAUDE_TOOL[opencodeTool.toLowerCase()] ?? (opencodeTool[0].toUpperCase() + opencodeTool.slice(1));
37
+ }
38
+ /**
39
+ * OpenCode tool 인자 키 → Claude tool_input 키. 가드가 읽는 필드만 정규화한다:
40
+ * - filePath → file_path (Read/Write/Edit 대상 파일)
41
+ * - command 는 그대로 (Bash) — db-guard 가 tool_input.command 로 rm -rf 감지.
42
+ * 나머지 키는 원본 유지(추가 정보는 무해).
43
+ */
44
+ export function normalizeToolArgs(args) {
45
+ if (!args || typeof args !== 'object')
46
+ return {};
47
+ const out = { ...args };
48
+ if ('filePath' in out && !('file_path' in out)) {
49
+ out.file_path = out.filePath;
50
+ }
51
+ return out;
52
+ }
53
+ /** OpenCode `tool.execute.before` (input.tool, output.args) → Claude PreToolUse stdin. */
54
+ export function toolBeforeToClaudeInput(opencodeTool, opencodeArgs) {
55
+ return {
56
+ hook_event_name: 'PreToolUse',
57
+ tool_name: mapToolName(opencodeTool),
58
+ tool_input: normalizeToolArgs(opencodeArgs),
59
+ };
60
+ }
61
+ /**
62
+ * forgen PreToolUse hook 의 stdout(JSON 여러 줄일 수 있음) → OpenCode 판정.
63
+ * permissionDecision:"deny" → block(throw). 그 외/파싱 실패 → 허용(fail-open — 가드 부재가
64
+ * 도구를 막지 않는다, forgen 의 hook 실패 정책과 동일).
65
+ */
66
+ export function decisionFromForgenOutput(stdout) {
67
+ const parsed = lastJsonObject(stdout);
68
+ if (!parsed || typeof parsed !== 'object')
69
+ return { block: false };
70
+ const hso = parsed.hookSpecificOutput;
71
+ if (hso && typeof hso === 'object') {
72
+ const decision = hso.permissionDecision;
73
+ if (decision === 'deny') {
74
+ const reason = hso.permissionDecisionReason;
75
+ return { block: true, reason: typeof reason === 'string' ? reason : '[forgen] blocked by guard' };
76
+ }
77
+ }
78
+ return { block: false };
79
+ }
80
+ /** 텍스트에서 마지막 JSON 객체를 파싱(hook 이 로그+JSON 을 섞어 낼 수 있음). */
81
+ function lastJsonObject(raw) {
82
+ if (!raw)
83
+ return null;
84
+ const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean);
85
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
86
+ if (!lines[i].startsWith('{'))
87
+ continue;
88
+ try {
89
+ return JSON.parse(lines[i]);
90
+ }
91
+ catch {
92
+ /* try previous line */
93
+ }
94
+ }
95
+ return null;
96
+ }
@@ -15,6 +15,13 @@
15
15
  */
16
16
  import type { HookEventInput, HookEventOutput } from '../core/types.js';
17
17
  import type { HostId, TrustLayerIntent } from '../core/trust-layer-intent.js';
18
+ /**
19
+ * Parity 는 *subprocess-hook* 형태 호스트(Claude/Codex) 간 projection 등가성만 검증한다.
20
+ * OpenCode(P1)는 in-process plugin 형태라 projection 이 아직 미구현(fail-loud 스텁)이므로
21
+ * parity corpus 범위에서 **명시적으로 제외** — 플러그인 슬림 착지 후 별도 parity 바인딩으로
22
+ * 편입한다(plan §4.3). 여기에 opencode 를 넣어 가짜 등가 주장을 만들지 않는다.
23
+ */
24
+ type ParityHost = Extract<HostId, 'claude' | 'codex'>;
18
25
  export interface BehavioralParityScenario {
19
26
  readonly id: string;
20
27
  /** 검증하려는 Trust Layer 의도. */
@@ -26,7 +33,7 @@ export interface BehavioralParityScenario {
26
33
  * 각 host 가 *내보낼 것으로 가정* 하는 raw 출력. P4 단계에서는 spec §18 source schema
27
34
  * 기반 직접 작성. P6 단계에서는 실 Codex CLI 출력으로 대체.
28
35
  */
29
- readonly hostRaw: Record<HostId, unknown>;
36
+ readonly hostRaw: Record<ParityHost, unknown>;
30
37
  /**
31
38
  * 사영 후 의미 동치성을 검증할 키들.
32
39
  * 예: ['continue', 'hookSpecificOutput.permissionDecision'].
@@ -44,7 +51,7 @@ export interface ParityCheckResult {
44
51
  codex: unknown;
45
52
  }>;
46
53
  /** 사영 결과 자체 (디버깅용). */
47
- readonly projected: Readonly<Record<HostId, HookEventOutput>>;
54
+ readonly projected: Readonly<Record<ParityHost, HookEventOutput>>;
48
55
  }
49
56
  export declare function runScenario(scenario: BehavioralParityScenario): ParityCheckResult;
50
57
  /**
@@ -60,3 +67,4 @@ export declare function runScenario(scenario: BehavioralParityScenario): ParityC
60
67
  * - suppress-output-equivalence : suppressOutput 동치성
61
68
  */
62
69
  export declare const SCENARIO_CORPUS: ReadonlyArray<BehavioralParityScenario>;
70
+ export {};
@@ -114,9 +114,20 @@ export const projectCodexToClaude = (raw, input) => {
114
114
  * 교체하면 되도록 — `getProjection(host)` 가 단일 진입점.)
115
115
  */
116
116
  export const projectClaudeToClaude = (raw, input) => projectCodexToClaude(raw, input);
117
+ /**
118
+ * OpenCode projection — P1 파운데이션 fail-loud 스텁.
119
+ * OpenCode 는 in-process plugin(throw/return)이라 subprocess stdout projection 과 형태가
120
+ * 다르다(plan §5 blocker 1). 실제 translation 은 plugin 슬림이 착지할 때 구현한다. 그 전까지
121
+ * 이 함수는 도달 불가(install-opencode 미구현 → getProjection('opencode') 미호출)이며,
122
+ * 혹시 호출되면 조용히 잘못된 결과를 내는 대신 명시적으로 실패한다.
123
+ */
124
+ const projectOpencodeToClaude = () => {
125
+ throw new Error('[forgen] OpenCode projection 미구현 — P1 plugin 슬림 착지 후 구현 예정 (plan §4.1 in-process-plugin binding).');
126
+ };
117
127
  const PROJECTIONS = {
118
128
  claude: projectClaudeToClaude,
119
129
  codex: projectCodexToClaude,
130
+ opencode: projectOpencodeToClaude,
120
131
  };
121
132
  export function getProjection(host) {
122
133
  const fn = PROJECTIONS[host];