@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,262 @@
1
+ /**
2
+ * Forgen v0.5.0 — `forgen migrate tenetx` (ADR-010 W1-1, Rule Reclaimer)
3
+ *
4
+ * tenetx(forgen 의 레거시 정체성) 및 구버전 forgen 이 ~/.claude/rules/ 와
5
+ * <cwd>/.claude/rules/ 에 남긴 규칙 파일 스프롤을 provenance 기반으로 회수한다.
6
+ * 2026-07-16 수동 청소(56K→0, 중복 스킬 21개 제거)의 프로덕트화.
7
+ *
8
+ * 판정 3단계 (설계: docs/plans/2026-07-16-v0.5.0-execution-plan.md W1-1):
9
+ * (a) content-hash 가 rendered-rules-manifest 와 일치 → 무프롬프트 삭제 (forgen 이 쓴 그대로)
10
+ * (b) provenance 마커만 매치 + hash 불일치 → --yes 필요 (사용자 편집 가능성)
11
+ * (c) 매치 없음 → 불간섭
12
+ *
13
+ * 안전장치: 모든 삭제는 백업 이동(가역), --dry-run 지원,
14
+ * settings.json/installed_plugins.json 편집은 --apply-settings 명시 동의 시에만.
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as os from 'node:os';
18
+ import * as path from 'node:path';
19
+ import { loadManifest, matchesManifest } from './rendered-rules-manifest.js';
20
+ import { acquireLock, releaseLock, atomicWriteFileSync } from './settings-lock.js';
21
+ // ── 판정 ──
22
+ /** tenetx / forge 계열 산출물 provenance 마커 (2026-07-16 실물 파일 기준) */
23
+ const PROVENANCE_MARKERS = Object.freeze([
24
+ /<!--\s*forge-tuned\s*-->/,
25
+ /^#\s*Tenetx\b/m,
26
+ /forge-generated v\d/,
27
+ /^# auto-generated from (observed interactions|profile \+ rule store)/m,
28
+ /^# Forgen —/m, // 구버전 forgen 이 글로벌에 복제한 project-context 류
29
+ ]);
30
+ /**
31
+ * 현행 렌더 타겟 — 프로젝트 .claude/rules/ 에서 forgen 이 매 세션 관리하는
32
+ * 파일들. reclaim 범위에서 제외한다 (manifest 생성 전 ⚠ 노이즈 방지;
33
+ * 삭제해도 다음 렌더에서 재생성되므로 회수의 의미도 없다).
34
+ * 정리 책임: 렌더(갱신) / uninstall(제거).
35
+ */
36
+ const CURRENT_RENDER_TARGETS = new Set([
37
+ 'project-context.md', 'v1-rules.md', 'forge-behavioral.md', 'user-profile.md',
38
+ ]);
39
+ function listRuleFiles(dir) {
40
+ try {
41
+ return fs.readdirSync(dir)
42
+ // .md 한정: tenetx/forgen 계열 rules 산출물은 전부 마크다운이었다
43
+ // (2026-07-16 실물 13개 파일 기준). 다른 확장자는 provenance 판단
44
+ // 근거가 없으므로 건드리지 않는다 — 불간섭이 기본값.
45
+ .filter(f => f.endsWith('.md'))
46
+ .map(f => path.join(dir, f));
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ export function hasProvenanceMarker(content) {
53
+ return PROVENANCE_MARKERS.some(re => re.test(content));
54
+ }
55
+ /** 스캔만 — 디스크 수정 없음. doctor --reclaim 이 그대로 사용. */
56
+ export function scanReclaimables(cwd, home = os.homedir()) {
57
+ const manifest = loadManifest(home);
58
+ const dirs = [
59
+ path.join(home, '.claude', 'rules'),
60
+ path.join(cwd, '.claude', 'rules'),
61
+ ];
62
+ const removable = [];
63
+ const needsConfirm = [];
64
+ const globalDir = dirs[0];
65
+ for (const dir of dirs) {
66
+ const isProjectDir = dir !== globalDir;
67
+ for (const p of listRuleFiles(dir)) {
68
+ // 프로젝트 스코프의 현행 렌더 타겟은 reclaim 범위 밖 (렌더/uninstall 책임)
69
+ if (isProjectDir && CURRENT_RENDER_TARGETS.has(path.basename(p)))
70
+ continue;
71
+ let content;
72
+ try {
73
+ content = fs.readFileSync(p, 'utf-8');
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ if (matchesManifest(p, content, manifest)) {
79
+ // (a) forgen 이 쓴 그대로임이 hash 로 증명됨 — 무프롬프트 회수.
80
+ // NOTE: 현행 harness 는 프로젝트 경로만 기록하므로 글로벌 파일은
81
+ // 오늘 기준 여기 도달하지 않는다(항상 marker 경로로 폴백). 이 분기는
82
+ // 미래에 렌더 대상이 바뀌어 특정 파일이 잔재화될 때를 위한 경로다.
83
+ removable.push({ path: p, reason: 'manifest-hash' });
84
+ continue;
85
+ }
86
+ if (hasProvenanceMarker(content)) {
87
+ needsConfirm.push({ path: p, reason: 'provenance-marker' });
88
+ }
89
+ // (c) 매치 없음 → 불간섭
90
+ }
91
+ }
92
+ return { removable, needsConfirm, settingsFindings: scanSettings(home) };
93
+ }
94
+ // ── settings / registry 탐지 ──
95
+ function scanSettings(home) {
96
+ const findings = [];
97
+ const settingsPath = path.join(home, '.claude', 'settings.json');
98
+ try {
99
+ const s = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
100
+ if (typeof s?.statusLine?.command === 'string' && s.statusLine.command.includes('tenetx')) {
101
+ findings.push(`settings.json statusLine.command = "${s.statusLine.command}" → "forgen statusline" 권장`);
102
+ }
103
+ for (const key of Object.keys(s?.enabledPlugins ?? {})) {
104
+ if (key.startsWith('tenetx@'))
105
+ findings.push(`settings.json enabledPlugins["${key}"] 활성 상태`);
106
+ }
107
+ if (Array.isArray(s?.plugins)) {
108
+ for (const p of s.plugins) {
109
+ if (typeof p === 'string' && p.includes('tenetx'))
110
+ findings.push(`settings.json plugins 배열에 ${p}`);
111
+ }
112
+ }
113
+ }
114
+ catch { /* settings 없음/파손 → 탐지 생략 */ }
115
+ const registryPath = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
116
+ try {
117
+ const r = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
118
+ for (const key of Object.keys(r?.plugins ?? {})) {
119
+ if (key.startsWith('tenetx@'))
120
+ findings.push(`installed_plugins.json 에 ${key} 등록 잔존`);
121
+ }
122
+ }
123
+ catch { /* ignore */ }
124
+ const pluginDir = path.join(home, '.claude', 'plugins', 'tenetx');
125
+ if (fs.existsSync(pluginDir)) {
126
+ findings.push(`플러그인 디렉토리 잔존: ${pluginDir}`);
127
+ }
128
+ return findings;
129
+ }
130
+ function applySettingsRemoval(home, backupDir) {
131
+ let applied = false;
132
+ // settings.json 은 코드베이스 불변식대로 settings-lock 하에서만 수정한다
133
+ // (settings-injector/uninstall 과 동일). 단 lock 은 실제 homedir 의
134
+ // settings.json 전용이므로, 테스트 주입 home 에서는 lock 을 생략한다.
135
+ const settingsPath = path.join(home, '.claude', 'settings.json');
136
+ const useLock = home === os.homedir();
137
+ if (useLock) {
138
+ try {
139
+ acquireLock();
140
+ }
141
+ catch {
142
+ return false; /* 활성 세션과 경합 — 건드리지 않음 */
143
+ }
144
+ }
145
+ try {
146
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
147
+ const s = JSON.parse(raw);
148
+ let changed = false;
149
+ if (typeof s?.statusLine?.command === 'string' && s.statusLine.command.includes('tenetx')) {
150
+ s.statusLine.command = 'forgen statusline';
151
+ changed = true;
152
+ }
153
+ for (const key of Object.keys(s?.enabledPlugins ?? {})) {
154
+ if (key.startsWith('tenetx@')) {
155
+ delete s.enabledPlugins[key];
156
+ changed = true;
157
+ }
158
+ }
159
+ if (Array.isArray(s?.plugins)) {
160
+ const before = s.plugins.length;
161
+ s.plugins = s.plugins.filter((p) => !(typeof p === 'string' && p.includes('tenetx')));
162
+ if (s.plugins.length !== before)
163
+ changed = true;
164
+ }
165
+ if (changed) {
166
+ fs.copyFileSync(settingsPath, path.join(backupDir, 'settings.json.bak'));
167
+ atomicWriteFileSync(settingsPath, `${JSON.stringify(s, null, 2)}\n`);
168
+ applied = true;
169
+ }
170
+ }
171
+ catch { /* settings 파손 시 건드리지 않음 */ }
172
+ finally {
173
+ if (useLock)
174
+ releaseLock();
175
+ }
176
+ const registryPath = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
177
+ try {
178
+ const raw = fs.readFileSync(registryPath, 'utf-8');
179
+ const r = JSON.parse(raw);
180
+ const tenetxKeys = Object.keys(r?.plugins ?? {}).filter(k => k.startsWith('tenetx@'));
181
+ if (tenetxKeys.length > 0) {
182
+ fs.copyFileSync(registryPath, path.join(backupDir, 'installed_plugins.json.bak'));
183
+ for (const k of tenetxKeys)
184
+ delete r.plugins[k];
185
+ atomicWriteFileSync(registryPath, `${JSON.stringify(r, null, 2)}\n`);
186
+ applied = true;
187
+ }
188
+ }
189
+ catch { /* ignore */ }
190
+ return applied;
191
+ }
192
+ // ── 실행 ──
193
+ export function runReclaim(opts) {
194
+ const home = opts.home ?? os.homedir();
195
+ const scan = scanReclaimables(opts.cwd, home);
196
+ const dryRun = opts.dryRun === true;
197
+ const targets = [
198
+ ...scan.removable,
199
+ ...(opts.yes ? scan.needsConfirm : []),
200
+ ];
201
+ const skippedNeedsConfirm = opts.yes ? [] : scan.needsConfirm.map(c => c.path);
202
+ let backupDir = null;
203
+ const removed = [];
204
+ if (!dryRun && (targets.length > 0 || opts.applySettings)) {
205
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
206
+ backupDir = path.join(home, '.forgen', 'backups', `reclaim-${ts}`);
207
+ fs.mkdirSync(backupDir, { recursive: true });
208
+ }
209
+ for (const t of targets) {
210
+ if (dryRun) {
211
+ removed.push(t.path);
212
+ continue;
213
+ }
214
+ try {
215
+ // 백업 이동 = 가역 삭제. 파일명 충돌 방지를 위해 dir 구조 평탄화 + 접두.
216
+ const flat = t.path.replace(/[\\/]/g, '_');
217
+ fs.copyFileSync(t.path, path.join(backupDir, flat));
218
+ fs.unlinkSync(t.path);
219
+ removed.push(t.path);
220
+ }
221
+ catch { /* 개별 실패는 건너뜀 — 나머지 회수 계속 */ }
222
+ }
223
+ let settingsApplied = false;
224
+ if (opts.applySettings && !dryRun && backupDir) {
225
+ settingsApplied = applySettingsRemoval(home, backupDir);
226
+ }
227
+ return {
228
+ removed,
229
+ skippedNeedsConfirm,
230
+ settingsFindings: scan.settingsFindings,
231
+ settingsApplied,
232
+ backupDir: dryRun ? null : backupDir,
233
+ dryRun,
234
+ };
235
+ }
236
+ // ── CLI 출력 ──
237
+ export function printReclaimResult(r) {
238
+ const mode = r.dryRun ? ' (dry-run — 디스크 미수정)' : '';
239
+ console.log(`\n [reclaim] tenetx/legacy 규칙 회수${mode}`);
240
+ if (r.removed.length === 0 && r.skippedNeedsConfirm.length === 0 && r.settingsFindings.length === 0) {
241
+ console.log(' ✓ 회수 대상 없음 — 깨끗한 상태');
242
+ return;
243
+ }
244
+ for (const p of r.removed) {
245
+ console.log(` ${r.dryRun ? '○ 회수 예정' : '✓ 회수'}: ${p}`);
246
+ }
247
+ for (const p of r.skippedNeedsConfirm) {
248
+ console.log(` ⚠ 마커는 있으나 forgen 이 쓴 내용과 다름 (사용자 편집 가능성): ${p}`);
249
+ console.log(' → 확인 후 삭제하려면 --yes 로 재실행');
250
+ }
251
+ if (r.settingsFindings.length > 0) {
252
+ console.log(' [settings] tenetx 흔적:');
253
+ for (const f of r.settingsFindings)
254
+ console.log(` - ${f}`);
255
+ if (r.settingsApplied)
256
+ console.log(' ✓ --apply-settings 로 제거 완료 (백업 저장됨)');
257
+ else
258
+ console.log(' → 자동 제거: forgen migrate tenetx --apply-settings');
259
+ }
260
+ if (r.backupDir)
261
+ console.log(` 백업: ${r.backupDir}`);
262
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * forgen probe-workflow — ADR-009 §1 결정적 미확인 변수 실측 도구.
2
+ * forgen dev probe-workflow — ADR-009 §1 결정적 미확인 변수 실측 도구.
3
3
  *
4
4
  * 질문: dynamic workflow **내부** 에이전트에 대해 SubagentStart/Stop·PostToolUse
5
5
  * 훅이 발화하는가? (워크플로우 런타임은 "대화와 분리된 격리 백그라운드"로
@@ -16,9 +16,9 @@
16
16
  * - (보조) hook-timing.jsonl 의 event 별 엔트리
17
17
  *
18
18
  * 절차 (2단계 — 런타임이 격리되어 있어 단일 프로세스로는 트리거 불가):
19
- * 1. `forgen probe-workflow arm` → baseline 마커 기록 + 안내 출력
19
+ * 1. `forgen dev probe-workflow arm` → baseline 마커 기록 + 안내 출력
20
20
  * 2. 사용자가 Claude Code 에서 워크플로우 1회 실행 (그 사이 다른 작업 금지)
21
- * 3. `forgen probe-workflow report` → baseline 이후 신호 수집 → verdict 박제
21
+ * 3. `forgen dev probe-workflow report` → baseline 이후 신호 수집 → verdict 박제
22
22
  *
23
23
  * 가정 (detailed-communication): arm~report 사이에 사용자가 **워크플로우만**
24
24
  * 실행했다고 전제한다. 일반 Task-tool subagent 를 같이 돌리면 신호가 섞인다.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * forgen probe-workflow — ADR-009 §1 결정적 미확인 변수 실측 도구.
2
+ * forgen dev probe-workflow — ADR-009 §1 결정적 미확인 변수 실측 도구.
3
3
  *
4
4
  * 질문: dynamic workflow **내부** 에이전트에 대해 SubagentStart/Stop·PostToolUse
5
5
  * 훅이 발화하는가? (워크플로우 런타임은 "대화와 분리된 격리 백그라운드"로
@@ -16,9 +16,9 @@
16
16
  * - (보조) hook-timing.jsonl 의 event 별 엔트리
17
17
  *
18
18
  * 절차 (2단계 — 런타임이 격리되어 있어 단일 프로세스로는 트리거 불가):
19
- * 1. `forgen probe-workflow arm` → baseline 마커 기록 + 안내 출력
19
+ * 1. `forgen dev probe-workflow arm` → baseline 마커 기록 + 안내 출력
20
20
  * 2. 사용자가 Claude Code 에서 워크플로우 1회 실행 (그 사이 다른 작업 금지)
21
- * 3. `forgen probe-workflow report` → baseline 이후 신호 수집 → verdict 박제
21
+ * 3. `forgen dev probe-workflow report` → baseline 이후 신호 수집 → verdict 박제
22
22
  *
23
23
  * 가정 (detailed-communication): arm~report 사이에 사용자가 **워크플로우만**
24
24
  * 실행했다고 전제한다. 일반 Task-tool subagent 를 같이 돌리면 신호가 섞인다.
@@ -188,14 +188,14 @@ function armProbe() {
188
188
  fs.mkdirSync(STATE_DIR, { recursive: true });
189
189
  fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2));
190
190
  console.log(`
191
- ${C.bold}forgen probe-workflow — armed${C.reset} ${C.dim}(${baseline.armedIso})${C.reset}
191
+ ${C.bold}forgen dev probe-workflow — armed${C.reset} ${C.dim}(${baseline.armedIso})${C.reset}
192
192
 
193
193
  ${C.cyan}다음을 정확히 순서대로 실행하세요:${C.reset}
194
194
  1. Claude Code (v2.1.154+, workflows 활성) 세션에서 ${C.bold}워크플로우 1회만${C.reset} 실행
195
195
  예: ${C.dim}Run a workflow to list files under src/${C.reset}
196
196
  또는: ${C.dim}/deep-research <질문>${C.reset}
197
197
  2. ${C.yellow}그 사이 다른 Task/subagent 작업은 돌리지 마세요${C.reset} (신호 오염 방지)
198
- 3. 워크플로우가 끝나면: ${C.bold}forgen probe-workflow report${C.reset}
198
+ 3. 워크플로우가 끝나면: ${C.bold}forgen dev probe-workflow report${C.reset}
199
199
 
200
200
  ${C.dim}전제: forgen 의 SubagentStart/Stop·PostToolUse 훅이 설치/활성 상태여야 합니다.
201
201
  불확실하면 'forgen config hooks' 로 확인하세요.${C.reset}
@@ -219,7 +219,7 @@ function colorForOutcome(outcome) {
219
219
  function reportProbe() {
220
220
  const baseline = loadBaseline();
221
221
  if (!baseline) {
222
- console.log(`\n ${C.red}✗ armed 상태가 아닙니다.${C.reset} 먼저 ${C.bold}forgen probe-workflow arm${C.reset} 를 실행하세요.\n`);
222
+ console.log(`\n ${C.red}✗ armed 상태가 아닙니다.${C.reset} 먼저 ${C.bold}forgen dev probe-workflow arm${C.reset} 를 실행하세요.\n`);
223
223
  process.exitCode = 1;
224
224
  return;
225
225
  }
@@ -228,7 +228,7 @@ function reportProbe() {
228
228
  persistResult(baseline, verdict);
229
229
  const oc = colorForOutcome(verdict.outcome);
230
230
  console.log(`
231
- ${C.bold}forgen probe-workflow — report${C.reset} ${C.dim}(armed ${baseline.armedIso})${C.reset}
231
+ ${C.bold}forgen dev probe-workflow — report${C.reset} ${C.dim}(armed ${baseline.armedIso})${C.reset}
232
232
 
233
233
  SubagentStart/Stop 발화 : ${verdict.subagentStartStopFired ? `${C.green}YES${C.reset}` : `${C.yellow}NO${C.reset}`}
234
234
  PostToolUse 발화 : ${verdict.postToolUseFired ? `${C.green}YES${C.reset}` : `${C.yellow}NO${C.reset}`}
@@ -254,8 +254,8 @@ function persistResult(baseline, verdict) {
254
254
  function statusProbe() {
255
255
  const baseline = loadBaseline();
256
256
  console.log(baseline
257
- ? `\n armed: ${C.cyan}${baseline.armedIso}${C.reset}\n → 워크플로우 실행 후 ${C.bold}forgen probe-workflow report${C.reset}\n`
258
- : `\n ${C.dim}armed 상태 아님.${C.reset} ${C.bold}forgen probe-workflow arm${C.reset} 로 시작하세요.\n`);
257
+ ? `\n armed: ${C.cyan}${baseline.armedIso}${C.reset}\n → 워크플로우 실행 후 ${C.bold}forgen dev probe-workflow report${C.reset}\n`
258
+ : `\n ${C.dim}armed 상태 아님.${C.reset} ${C.bold}forgen dev probe-workflow arm${C.reset} 로 시작하세요.\n`);
259
259
  }
260
260
  export async function handleProbeWorkflow(args) {
261
261
  const sub = args[0] ?? 'status';
@@ -271,12 +271,12 @@ export async function handleProbeWorkflow(args) {
271
271
  return;
272
272
  default:
273
273
  console.log(`
274
- ${C.bold}forgen probe-workflow${C.reset} — ADR-009 §1: 워크플로우 훅 발화 실측
274
+ ${C.bold}forgen dev probe-workflow${C.reset} — ADR-009 §1: 워크플로우 훅 발화 실측
275
275
 
276
276
  Usage:
277
- forgen probe-workflow arm baseline 기록 + 안내 (먼저)
278
- forgen probe-workflow report 워크플로우 실행 후 신호 수집 → 판정
279
- forgen probe-workflow status 현재 armed 상태 확인
277
+ forgen dev probe-workflow arm baseline 기록 + 안내 (먼저)
278
+ forgen dev probe-workflow report 워크플로우 실행 후 신호 수집 → 판정
279
+ forgen dev probe-workflow status 현재 armed 상태 확인
280
280
  `);
281
281
  }
282
282
  }
@@ -107,7 +107,7 @@ export async function handleRecall(args) {
107
107
  return;
108
108
  }
109
109
  console.log('');
110
- console.log(` forgen recall — last ${recalls.length} surfaced solution${recalls.length === 1 ? '' : 's'}`);
110
+ console.log(` forgen status --compound — last ${recalls.length} surfaced solution${recalls.length === 1 ? '' : 's'}`);
111
111
  console.log(' ─────────────────────────────────────────');
112
112
  for (const r of recalls) {
113
113
  const score = r.match_score !== undefined ? ` @${r.match_score.toFixed(2)}` : '';
@@ -27,7 +27,7 @@ export async function handleRegressMap(args = []) {
27
27
  console.log('regress-map: git unavailable or no commits in window.');
28
28
  return;
29
29
  }
30
- console.log(' ┌─ forgen regress-map ───────────────────────────────────┐');
30
+ console.log(' ┌─ forgen dev regress-map ───────────────────────────────────┐');
31
31
  console.log(` │ window: last ${map.windowDays} days · fix commits: ${map.fixCommits}`.padEnd(60) + '│');
32
32
  if (ratio.available) {
33
33
  console.log(` │ ${formatFixRatio(ratio)}`.padEnd(60) + '│');
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Forgen v0.5.0 — Rendered Rules Manifest (ADR-010 W1-1)
3
+ *
4
+ * harness 가 렌더해 쓴 규칙 파일의 content-hash 원장.
5
+ * reclaimer(`forgen migrate tenetx`)가 "forgen 이 쓴 그대로인 파일"과
6
+ * "사용자가 편집했거나 다른 도구가 쓴 파일"을 구분하는 유일한 근거다 —
7
+ * native /doctor 는 비용만 알지 provenance 를 모른다.
8
+ *
9
+ * 성능 (Rev 2): 렌더 시점에 이미 메모리에 있는 내용을 해시하므로
10
+ * 재읽기/전체 스캔 비용이 없다. 스캔 사이드(reclaimer)만 파일을 읽는다.
11
+ */
12
+ export interface ManifestEntry {
13
+ sha256: string;
14
+ version: string;
15
+ ts: string;
16
+ }
17
+ export type RulesManifest = Record<string, ManifestEntry>;
18
+ export declare function manifestPath(home?: string): string;
19
+ export declare function sha256Of(content: string): string;
20
+ export declare function loadManifest(home?: string): RulesManifest;
21
+ /**
22
+ * 렌더 직후 호출 — 쓴 파일들의 해시를 기존 manifest 에 병합 기록.
23
+ * 같은 경로는 최신 해시로 **덮어쓴다** (history 없음): 같은 경로에 구버전
24
+ * 내용이 남는 시나리오는 렌더가 즉시 갱신하므로 존재하지 않고, 다른 경로의
25
+ * 구버전 파일은 marker-only(needs-confirm) 경로로 안전 강등된다.
26
+ */
27
+ export declare function recordRenderedFiles(files: Record<string, string>, version: string, home?: string): void;
28
+ /** 파일 내용이 manifest 의 어떤 엔트리와도 일치하는가 (경로 키 기준) */
29
+ export declare function matchesManifest(absPath: string, content: string, manifest: RulesManifest): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Forgen v0.5.0 — Rendered Rules Manifest (ADR-010 W1-1)
3
+ *
4
+ * harness 가 렌더해 쓴 규칙 파일의 content-hash 원장.
5
+ * reclaimer(`forgen migrate tenetx`)가 "forgen 이 쓴 그대로인 파일"과
6
+ * "사용자가 편집했거나 다른 도구가 쓴 파일"을 구분하는 유일한 근거다 —
7
+ * native /doctor 는 비용만 알지 provenance 를 모른다.
8
+ *
9
+ * 성능 (Rev 2): 렌더 시점에 이미 메모리에 있는 내용을 해시하므로
10
+ * 재읽기/전체 스캔 비용이 없다. 스캔 사이드(reclaimer)만 파일을 읽는다.
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as os from 'node:os';
14
+ import * as path from 'node:path';
15
+ import { createHash } from 'node:crypto';
16
+ export function manifestPath(home = os.homedir()) {
17
+ return path.join(home, '.forgen', 'state', 'rendered-rules-manifest.json');
18
+ }
19
+ export function sha256Of(content) {
20
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
21
+ }
22
+ export function loadManifest(home = os.homedir()) {
23
+ try {
24
+ const raw = fs.readFileSync(manifestPath(home), 'utf-8');
25
+ const parsed = JSON.parse(raw);
26
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ /**
33
+ * 렌더 직후 호출 — 쓴 파일들의 해시를 기존 manifest 에 병합 기록.
34
+ * 같은 경로는 최신 해시로 **덮어쓴다** (history 없음): 같은 경로에 구버전
35
+ * 내용이 남는 시나리오는 렌더가 즉시 갱신하므로 존재하지 않고, 다른 경로의
36
+ * 구버전 파일은 marker-only(needs-confirm) 경로로 안전 강등된다.
37
+ */
38
+ export function recordRenderedFiles(files, version, home = os.homedir()) {
39
+ try {
40
+ const p = manifestPath(home);
41
+ fs.mkdirSync(path.dirname(p), { recursive: true });
42
+ const manifest = loadManifest(home);
43
+ const ts = new Date().toISOString();
44
+ for (const [absPath, content] of Object.entries(files)) {
45
+ manifest[absPath] = { sha256: sha256Of(content), version, ts };
46
+ }
47
+ fs.writeFileSync(p, `${JSON.stringify(manifest, null, 2)}\n`);
48
+ }
49
+ catch {
50
+ // manifest 기록 실패는 렌더를 막지 않는다 (fail-open) —
51
+ // 그 경우 reclaimer 가 marker-only 경로로 폴백한다.
52
+ }
53
+ }
54
+ /** 파일 내용이 manifest 의 어떤 엔트리와도 일치하는가 (경로 키 기준) */
55
+ export function matchesManifest(absPath, content, manifest) {
56
+ const entry = manifest[absPath];
57
+ if (!entry)
58
+ return false;
59
+ return entry.sha256 === sha256Of(content);
60
+ }
@@ -11,6 +11,7 @@ import * as path from 'node:path';
11
11
  import { createLogger } from './logger.js';
12
12
  import { FORGEN_HOME } from './paths.js';
13
13
  import { ensureObservabilitySchema } from './observability-store.js';
14
+ import { stripPrivate } from '../engine/private-filter.js';
14
15
  const require = createRequire(import.meta.url);
15
16
  // Suppress ExperimentalWarning for node:sqlite (Node.js 22+)
16
17
  {
@@ -116,8 +117,13 @@ export async function indexCodexSession(cwd, transcriptPath, sessionId) {
116
117
  const role = entry.payload.role;
117
118
  if (role !== 'user' && role !== 'assistant')
118
119
  continue;
119
- const text = extractCodexText(entry.payload.content);
120
- if (!text)
120
+ const rawText = extractCodexText(entry.payload.content);
121
+ if (!rawText)
122
+ continue;
123
+ // W2-5 (private 태그): FTS 인덱스는 session-search 로 재노출되므로 <private>
124
+ // 범위를 인덱싱 전에 제거한다. 통째 private 메시지는 인덱싱하지 않는다.
125
+ const text = stripPrivate(rawText).cleaned;
126
+ if (!text.trim())
121
127
  continue;
122
128
  const truncated = text.slice(0, 10000);
123
129
  const ts = typeof entry.timestamp === 'string' ? entry.timestamp : '';
@@ -179,7 +185,12 @@ export async function indexSession(cwd, transcriptPath, sessionId) {
179
185
  : '';
180
186
  }
181
187
  if (role && text) {
182
- const truncated = text.slice(0, 10000);
188
+ // W2-5 (private 태그): FTS 인덱스가 session-search 로 재노출되므로 <private>
189
+ // 범위를 제거 후 인덱싱. 통째 private 메시지는 스킵.
190
+ const cleanText = stripPrivate(text).cleaned;
191
+ if (!cleanText.trim())
192
+ continue;
193
+ const truncated = cleanText.slice(0, 10000);
183
194
  const result = insertMsg.run(sessionId, role, truncated, entry.timestamp ?? '');
184
195
  // FTS5 인덱스 동기화
185
196
  if (fts5Available) {
@@ -6,6 +6,9 @@
6
6
  */
7
7
  import type { RuntimeHost } from './types.js';
8
8
  import type { V1BootstrapResult } from './v1-bootstrap.js';
9
+ export declare function stripForgenManagedRules(rules: string[]): string[];
10
+ /** Apply v1 trust policy → permissions (deny/ask lists). */
11
+ export declare function applyTrustPolicyPermissions(settings: Record<string, unknown>, v1Result: V1BootstrapResult): void;
9
12
  /**
10
13
  * Inject forgen settings into Claude Code settings.json.
11
14
  * Coordinates: read/backup → env merge → statusLine → hooks → trust policy → atomic write.
@@ -12,13 +12,20 @@ import { ConfigError } from './errors.js';
12
12
  import { createLogger } from './logger.js';
13
13
  import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, readSettingsSafely, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
14
14
  const log = createLogger('settings-injector');
15
+ // 주의(2026-08-04 버그수정): '# forgen-managed' 는 **더 이상 주입하지 않는다** — JSON
16
+ // permissions 배열엔 주석이 데이터 원소가 되어 Claude Code 가 malformed deny 룰로 경고했다.
17
+ // strip-set 에는 남겨둬, 이전 버그로 오염된 사용자 settings 가 재주입 시 self-heal 되게 한다.
18
+ // 멱등성은 마커가 아니라 아래 실제 룰 문자열의 정확일치 제거로 이미 보장된다.
15
19
  const FORGEN_PERMISSION_RULES = new Set([
16
- '# forgen-managed',
20
+ '# forgen-managed', // legacy 오염 정리용 (신규 주입 안 함)
17
21
  'Bash(rm -rf *)',
18
22
  'Bash(git push --force*)',
19
23
  'Bash(git reset --hard*)',
20
24
  ]);
21
- function stripForgenManagedRules(rules) {
25
+ /** 신규 주입할 forgen deny 룰 (마커 없음). */
26
+ const FORGEN_DENY_RULES = ['Bash(rm -rf *)', 'Bash(git push --force*)', 'Bash(git reset --hard*)'];
27
+ const FORGEN_ASK_RULES = ['Bash(rm -rf *)', 'Bash(git push --force*)'];
28
+ export function stripForgenManagedRules(rules) {
22
29
  return rules.filter((r) => !FORGEN_PERMISSION_RULES.has(r));
23
30
  }
24
31
  /**
@@ -116,29 +123,18 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
116
123
  }
117
124
  }
118
125
  /** Apply v1 trust policy → permissions (deny/ask lists). */
119
- function applyTrustPolicyPermissions(settings, v1Result) {
126
+ export function applyTrustPolicyPermissions(settings, v1Result) {
120
127
  if (!v1Result.session)
121
128
  return;
122
129
  const trust = v1Result.session.effective_trust_policy;
123
130
  const permissions = settings.permissions ?? {};
124
131
  const existingDeny = stripForgenManagedRules(permissions.deny ?? []);
125
132
  if (trust === '가드레일 우선') {
126
- permissions.deny = [
127
- ...existingDeny,
128
- '# forgen-managed',
129
- 'Bash(rm -rf *)',
130
- 'Bash(git push --force*)',
131
- 'Bash(git reset --hard*)',
132
- ];
133
+ permissions.deny = [...existingDeny, ...FORGEN_DENY_RULES];
133
134
  }
134
135
  else if (trust === '승인 완화') {
135
136
  const existingAsk = stripForgenManagedRules(permissions.ask ?? []);
136
- permissions.ask = [
137
- ...existingAsk,
138
- '# forgen-managed',
139
- 'Bash(rm -rf *)',
140
- 'Bash(git push --force*)',
141
- ];
137
+ permissions.ask = [...existingAsk, ...FORGEN_ASK_RULES];
142
138
  permissions.deny = existingDeny.length > 0 ? existingDeny : undefined;
143
139
  }
144
140
  // '완전 신뢰 실행': 추가 제한 없음
@@ -1,5 +1,7 @@
1
1
  import type { V1HarnessContext } from './harness.js';
2
2
  import type { RuntimeHost } from './types.js';
3
+ /** runAutoCompound 결과 — sweep(무인 cron)이 실제 spawn 만 기록하도록 상태 반환. */
4
+ export type AutoCompoundStatus = 'spawned' | 'skipped-inflight' | 'skipped-dedup' | 'failed';
3
5
  /**
4
6
  * 세션 종료 후 자동 compound 추출을 백그라운드(detached)로 시작.
5
7
  *
@@ -12,7 +14,15 @@ import type { RuntimeHost } from './types.js';
12
14
  * dedup: last-auto-compound.json 을 Stop 훅과 공유 — 같은 세션의 최근 run 이 있으면
13
15
  * skip 하여 detached spawn 의 double-run 을 방지한다.
14
16
  */
15
- export declare function runAutoCompound(cwd: string, transcriptPath: string, sessionId: string): void;
17
+ /**
18
+ * per-session in-flight 슬롯 확보 (spawn *전* 표식). 이미 in-flight(TTL 내)면 false 반환 → skip.
19
+ * runAutoCompound 와 context-guard.maybeSpawnAutoCompound 가 **공유**해 이중 spawn(중복 Haiku
20
+ * 과금)을 막는다 (리뷰 SEV-2 #3: 완료-게이트 단일슬롯만으로는 부족).
21
+ */
22
+ export declare function claimAutoCompoundInflight(sessionId: string, now?: number): boolean;
23
+ /** in-flight 슬롯 해제 (spawn 실패 시 재시도 가능하도록). */
24
+ export declare function releaseAutoCompoundInflight(sessionId: string): void;
25
+ export declare function runAutoCompound(cwd: string, transcriptPath: string, sessionId: string, promptCount?: number): AutoCompoundStatus;
16
26
  /**
17
27
  * Plan B-1: 세션 transcript 사후 스캔으로 rate-limit 감지.
18
28
  *