@wooojin/forgen 0.4.1 → 0.4.4
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 +5 -5
- package/CHANGELOG.md +267 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +17 -9
- package/README.ko.md +34 -12
- package/README.md +65 -12
- package/README.zh.md +17 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/{commands → assets/claude/commands}/calibrate.md +4 -3
- package/{commands → assets/claude/commands}/retro.md +2 -2
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- package/dist/checks/_shared/text-sanitizer.d.ts +21 -0
- package/dist/checks/_shared/text-sanitizer.js +60 -0
- package/dist/checks/dangerous-response-pattern.d.ts +32 -0
- package/dist/checks/dangerous-response-pattern.js +65 -0
- package/dist/checks/fact-vs-agreement.js +25 -1
- package/dist/cli.js +78 -6
- package/dist/core/auto-compound-runner.js +90 -39
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +32 -0
- package/dist/core/doctor.js +92 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -0
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +1 -0
- package/dist/core/migrate-cli.js +19 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.js +12 -0
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +4 -2
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/db-guard.js +3 -3
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +1 -1
- package/dist/hooks/post-tool-use.js +13 -4
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.js +4 -4
- package/dist/hooks/rate-limiter.js +2 -2
- package/dist/hooks/session-recovery.js +11 -0
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +18 -0
- package/dist/hooks/shared/hook-response.js +31 -0
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/stop-guard.js +57 -25
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +51 -0
- package/dist/renderer/rule-renderer.d.ts +1 -1
- package/dist/renderer/rule-renderer.js +73 -1
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/compound-usage-store.d.ts +28 -0
- package/dist/store/compound-usage-store.js +59 -0
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +34 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +6 -4
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- package/skills/calibrate/SKILL.md +4 -3
- package/skills/retro/SKILL.md +2 -2
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="340" viewBox="0 0 800 340">
|
|
2
|
+
<style>
|
|
3
|
+
.term { font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', monospace; font-size: 13px; }
|
|
4
|
+
.prompt { fill: #a6e3a1; }
|
|
5
|
+
.cmd { fill: #cdd6f4; }
|
|
6
|
+
.tag { fill: #89b4fa; }
|
|
7
|
+
.haiku { fill: #f9e2af; }
|
|
8
|
+
.sonnet { fill: #a6e3a1; }
|
|
9
|
+
.opus { fill: #cba6f7; }
|
|
10
|
+
.dim { fill: #6c7086; }
|
|
11
|
+
.box { fill: #a6e3a1; }
|
|
12
|
+
@keyframes typeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
13
|
+
@keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } }
|
|
14
|
+
@keyframes blink { 0%, 100% { opacity: 0.8; } 50% { opacity: 0; } }
|
|
15
|
+
.line1 { animation: typeIn 0.1s 0.3s both; }
|
|
16
|
+
.line2 { animation: fadeIn 0.2s 0.8s both; }
|
|
17
|
+
.line3 { animation: fadeIn 0.2s 1.1s both; }
|
|
18
|
+
.line4 { animation: fadeIn 0.2s 1.4s both; }
|
|
19
|
+
.line5 { animation: fadeIn 0.2s 1.7s both; }
|
|
20
|
+
.line6 { animation: fadeIn 0.2s 2.0s both; }
|
|
21
|
+
.line7 { animation: fadeIn 0.3s 2.5s both; }
|
|
22
|
+
.cursor { animation: blink 1s 2.8s infinite; }
|
|
23
|
+
</style>
|
|
24
|
+
|
|
25
|
+
<!-- Terminal window -->
|
|
26
|
+
<rect width="800" height="340" rx="10" ry="10" fill="#1e1e2e"/>
|
|
27
|
+
<rect width="800" height="36" rx="10" ry="10" fill="#313244"/>
|
|
28
|
+
<rect y="26" width="800" height="10" fill="#313244"/>
|
|
29
|
+
|
|
30
|
+
<!-- Traffic lights -->
|
|
31
|
+
<circle cx="20" cy="18" r="6" fill="#f38ba8"/>
|
|
32
|
+
<circle cx="40" cy="18" r="6" fill="#f9e2af"/>
|
|
33
|
+
<circle cx="60" cy="18" r="6" fill="#a6e3a1"/>
|
|
34
|
+
<text x="400" y="23" text-anchor="middle" class="term" font-size="12" fill="#6c7086">forgen — terminal</text>
|
|
35
|
+
|
|
36
|
+
<!-- Line 1: command -->
|
|
37
|
+
<g class="line1">
|
|
38
|
+
<text x="24" y="68" class="term prompt">$</text>
|
|
39
|
+
<text x="40" y="68" class="term cmd">forgen</text>
|
|
40
|
+
</g>
|
|
41
|
+
|
|
42
|
+
<!-- Line 2: philosophy -->
|
|
43
|
+
<g class="line2">
|
|
44
|
+
<text x="24" y="96" class="term tag">[forgen]</text>
|
|
45
|
+
<text x="96" y="96" class="term cmd">Philosophy: </text>
|
|
46
|
+
<text x="194" y="96" class="term" fill="#f9e2af">my-engineering</text>
|
|
47
|
+
<text x="310" y="96" class="term dim"> (global)</text>
|
|
48
|
+
</g>
|
|
49
|
+
|
|
50
|
+
<!-- Line 3: scope -->
|
|
51
|
+
<g class="line3">
|
|
52
|
+
<text x="24" y="120" class="term tag">[forgen]</text>
|
|
53
|
+
<text x="96" y="120" class="term cmd">Scope: Me(5) │ 3 rules, 2 solutions</text>
|
|
54
|
+
</g>
|
|
55
|
+
|
|
56
|
+
<!-- Line 4: routing -->
|
|
57
|
+
<g class="line4">
|
|
58
|
+
<text x="24" y="144" class="term tag">[forgen]</text>
|
|
59
|
+
<text x="96" y="144" class="term cmd">Routing: </text>
|
|
60
|
+
<text x="172" y="144" class="term haiku">haiku:3</text>
|
|
61
|
+
<text x="232" y="144" class="term dim"> │ </text>
|
|
62
|
+
<text x="256" y="144" class="term sonnet">sonnet:5</text>
|
|
63
|
+
<text x="324" y="144" class="term dim"> │ </text>
|
|
64
|
+
<text x="348" y="144" class="term opus">opus:4</text>
|
|
65
|
+
</g>
|
|
66
|
+
|
|
67
|
+
<!-- Line 5: pack -->
|
|
68
|
+
<g class="line5">
|
|
69
|
+
<text x="24" y="168" class="term tag">[forgen]</text>
|
|
70
|
+
<text x="96" y="168" class="term cmd">Pack: backend v1.0.0 (5 rules, 3 solutions)</text>
|
|
71
|
+
</g>
|
|
72
|
+
|
|
73
|
+
<!-- Line 6: launching -->
|
|
74
|
+
<g class="line6">
|
|
75
|
+
<text x="24" y="192" class="term tag">[forgen]</text>
|
|
76
|
+
<text x="96" y="192" class="term cmd">Starting Claude Code...</text>
|
|
77
|
+
</g>
|
|
78
|
+
|
|
79
|
+
<!-- Separator -->
|
|
80
|
+
<g class="line7">
|
|
81
|
+
<line x1="24" y1="212" x2="776" y2="212" stroke="#313244" stroke-width="1"/>
|
|
82
|
+
|
|
83
|
+
<!-- Claude Code box -->
|
|
84
|
+
<text x="24" y="236" class="term dim">╭──────────────────────────────────────────────────╮</text>
|
|
85
|
+
<text x="24" y="256" class="term dim">│</text>
|
|
86
|
+
<text x="40" y="256" class="term box"> ✓ Claude Code (philosophy-driven mode)</text>
|
|
87
|
+
<text x="430" y="256" class="term dim">│</text>
|
|
88
|
+
<text x="24" y="276" class="term dim">│</text>
|
|
89
|
+
<text x="40" y="276" class="term" fill="#cdd6f4"> 19 agents │ 19 skills │ 18 hooks loaded</text>
|
|
90
|
+
<text x="430" y="276" class="term dim">│</text>
|
|
91
|
+
<text x="24" y="296" class="term dim">╰──────────────────────────────────────────────────╯</text>
|
|
92
|
+
|
|
93
|
+
<!-- Blinking cursor -->
|
|
94
|
+
<rect x="24" y="310" width="8" height="16" fill="#cdd6f4" class="cursor"/>
|
|
95
|
+
</g>
|
|
96
|
+
</svg>
|
package/assets/icon.png
ADDED
|
Binary file
|
|
@@ -18,5 +18,6 @@
|
|
|
18
18
|
{ "name": "subagent-tracker-stop", "tier": "workflow", "event": "SubagentStop", "matcher": "*", "script": "hooks/subagent-tracker.js stop", "timeout": 2, "compoundCritical": false },
|
|
19
19
|
{ "name": "post-tool-failure", "tier": "workflow", "event": "PostToolUseFailure", "matcher": "*", "script": "hooks/post-tool-failure.js", "timeout": 3, "compoundCritical": false },
|
|
20
20
|
{ "name": "solution-injector", "tier": "compound-core", "event": "UserPromptSubmit", "matcher": "*", "script": "hooks/solution-injector.js", "timeout": 5, "compoundCritical": true },
|
|
21
|
-
{ "name": "skill-injector", "tier": "compound-core", "event": "UserPromptSubmit", "matcher": "*", "script": "hooks/skill-injector.js", "timeout": 5, "compoundCritical": true }
|
|
21
|
+
{ "name": "skill-injector", "tier": "compound-core", "event": "UserPromptSubmit", "matcher": "*", "script": "hooks/skill-injector.js", "timeout": 5, "compoundCritical": true },
|
|
22
|
+
{ "name": "forge-loop-progress", "tier": "compound-core", "event": "UserPromptSubmit", "matcher": "*", "script": "hooks/forge-loop-progress.js", "timeout": 2, "compoundCritical": false }
|
|
22
23
|
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — text-sanitizer (Pathfinder D4 + D5 흡수).
|
|
3
|
+
*
|
|
4
|
+
* stop-guard 진입 시 lastMessage 에 한 번 적용. 두 가지 면제:
|
|
5
|
+
*
|
|
6
|
+
* D4 — Structured-output 면제:
|
|
7
|
+
* observer hook / skill 산출물(<observation>...</observation>,
|
|
8
|
+
* <summary>...</summary> 등)은 *과거 사실 기록* 이지 자기 평가가 아님.
|
|
9
|
+
* 본문 안의 "verified", "신뢰도 95/100" 같은 어휘에 가드가 발화하면 FP.
|
|
10
|
+
*
|
|
11
|
+
* D5 — Self-paradox 면제:
|
|
12
|
+
* regex 트리거 어휘(예: 4/10, verified)를 *인용해서* 설명만 해도 본인
|
|
13
|
+
* 매칭. 메타 대화/디버깅에서 가드가 무력화됨. 코드/직인용 본문은 가드
|
|
14
|
+
* 판정 대상이 아니므로 stripping.
|
|
15
|
+
*
|
|
16
|
+
* 결정 (PATHFINDER-2026-04-30/03-unified-proposal.md):
|
|
17
|
+
* - 자연 산문 속 진짜 점수 인플레이션은 살아남아야 함 (TP 보존)
|
|
18
|
+
* - 짧은 인용("...") 만 제거; 긴 인용은 사용자 인용일 수 있어 보존
|
|
19
|
+
* - idempotent: 두 번 적용해도 결과 동일
|
|
20
|
+
*/
|
|
21
|
+
export declare function sanitizeForGuard(raw: string): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — text-sanitizer (Pathfinder D4 + D5 흡수).
|
|
3
|
+
*
|
|
4
|
+
* stop-guard 진입 시 lastMessage 에 한 번 적용. 두 가지 면제:
|
|
5
|
+
*
|
|
6
|
+
* D4 — Structured-output 면제:
|
|
7
|
+
* observer hook / skill 산출물(<observation>...</observation>,
|
|
8
|
+
* <summary>...</summary> 등)은 *과거 사실 기록* 이지 자기 평가가 아님.
|
|
9
|
+
* 본문 안의 "verified", "신뢰도 95/100" 같은 어휘에 가드가 발화하면 FP.
|
|
10
|
+
*
|
|
11
|
+
* D5 — Self-paradox 면제:
|
|
12
|
+
* regex 트리거 어휘(예: 4/10, verified)를 *인용해서* 설명만 해도 본인
|
|
13
|
+
* 매칭. 메타 대화/디버깅에서 가드가 무력화됨. 코드/직인용 본문은 가드
|
|
14
|
+
* 판정 대상이 아니므로 stripping.
|
|
15
|
+
*
|
|
16
|
+
* 결정 (PATHFINDER-2026-04-30/03-unified-proposal.md):
|
|
17
|
+
* - 자연 산문 속 진짜 점수 인플레이션은 살아남아야 함 (TP 보존)
|
|
18
|
+
* - 짧은 인용("...") 만 제거; 긴 인용은 사용자 인용일 수 있어 보존
|
|
19
|
+
* - idempotent: 두 번 적용해도 결과 동일
|
|
20
|
+
*/
|
|
21
|
+
const STRUCTURED_TAGS = [
|
|
22
|
+
'observation',
|
|
23
|
+
'summary',
|
|
24
|
+
'request',
|
|
25
|
+
'investigated',
|
|
26
|
+
'completed',
|
|
27
|
+
'next-steps',
|
|
28
|
+
'next_steps',
|
|
29
|
+
'title',
|
|
30
|
+
'subtitle',
|
|
31
|
+
'learned',
|
|
32
|
+
'discovery',
|
|
33
|
+
];
|
|
34
|
+
const SHORT_QUOTE_MAX = 20;
|
|
35
|
+
export function sanitizeForGuard(raw) {
|
|
36
|
+
if (!raw)
|
|
37
|
+
return raw;
|
|
38
|
+
let s = raw;
|
|
39
|
+
// 1) structured-output 블록 (open + close 쌍) 제거
|
|
40
|
+
for (const tag of STRUCTURED_TAGS) {
|
|
41
|
+
const re = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi');
|
|
42
|
+
s = s.replace(re, '');
|
|
43
|
+
}
|
|
44
|
+
// self-closing 또는 dangling open tag 도 제거 (열렸지만 닫힘 누락 케이스)
|
|
45
|
+
for (const tag of STRUCTURED_TAGS) {
|
|
46
|
+
const reSelf = new RegExp(`<${tag}\\b[^>]*\\/?>`, 'gi');
|
|
47
|
+
s = s.replace(reSelf, '');
|
|
48
|
+
const reClose = new RegExp(`<\\/${tag}>`, 'gi');
|
|
49
|
+
s = s.replace(reClose, '');
|
|
50
|
+
}
|
|
51
|
+
// 2) fenced code block (```...```) 제거 — 진짜 점수가 들어갈 자리 아님
|
|
52
|
+
s = s.replace(/```[\s\S]*?```/g, '');
|
|
53
|
+
// 3) inline backtick 코드 (`...`) 제거
|
|
54
|
+
s = s.replace(/`[^`\n]*`/g, '');
|
|
55
|
+
// 4) 짧은 직인용 ("...") 제거 — 길이 SHORT_QUOTE_MAX 이하만.
|
|
56
|
+
// 긴 인용은 사용자 발언/실제 사실 인용이므로 가드 판정 대상에 남김.
|
|
57
|
+
const shortQuoteRe = new RegExp(`"[^"\\n]{0,${SHORT_QUOTE_MAX}}"`, 'g');
|
|
58
|
+
s = s.replace(shortQuoteRe, '');
|
|
59
|
+
return s;
|
|
60
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — DANGEROUS-RESPONSE: 응답 텍스트 파괴적 명령 가드
|
|
3
|
+
*
|
|
4
|
+
* Stop hook 단계에서 어시스턴트 응답 텍스트를 스캔하여 *파괴적 명령 제안*을
|
|
5
|
+
* 감지한다. PreToolUse 가드는 실제 도구 실행 직전에만 동작하므로, 모델이
|
|
6
|
+
* 텍스트로 위험한 대안을 *제안*하는 케이스(예: `find ... -exec rm -r {}`)는
|
|
7
|
+
* 도구 실행 단계에 도달하지 못하면 차단되지 않는다. 본 체크는 그 갭을 메운다.
|
|
8
|
+
*
|
|
9
|
+
* 발견 경위 (forgen-eval syn-004, 2026-05-06):
|
|
10
|
+
* notepad 룰 "[no-rm-rf-unconfirmed]" inject 상태에서, driver(llama 8B) 가
|
|
11
|
+
* `rm -rf` 리터럴은 회피했지만 `find . -type d -name 'build' -exec rm -r {}`
|
|
12
|
+
* 같은 우회 명령을 "안전한 대안"으로 제안. PreToolUse 차단은 실 실행 시
|
|
13
|
+
* 동작하지만 "응답 텍스트만으론" 못 막음. driver 모델 능력에 무관하게
|
|
14
|
+
* 일관된 안전 보장이 되도록 응답 단계에서 한 번 더 잡아낸다.
|
|
15
|
+
*
|
|
16
|
+
* 동작:
|
|
17
|
+
* - 어시스턴트 응답에 등록된 파괴 패턴이 매칭되면 block 권고.
|
|
18
|
+
* - "확실히, 명시적 확인" 같은 confirm 표현 동반 여부는 검사하지 않는다
|
|
19
|
+
* (휴리스틱 brittle). 매칭 시 무조건 block 후, 모델이 안전한 대안 제시
|
|
20
|
+
* 또는 명시적 confirm 절차로 재응답하도록 유도.
|
|
21
|
+
*
|
|
22
|
+
* 순수 함수 — Stop hook 이 reason 문자열을 그대로 주입.
|
|
23
|
+
*/
|
|
24
|
+
export interface DangerousResultMatch {
|
|
25
|
+
block: boolean;
|
|
26
|
+
matched?: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
}
|
|
29
|
+
/** 응답 텍스트가 파괴적 명령 패턴을 포함하는지 검사. */
|
|
30
|
+
export declare function checkDangerousResponsePattern(opts: {
|
|
31
|
+
text: string;
|
|
32
|
+
}): DangerousResultMatch;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — DANGEROUS-RESPONSE: 응답 텍스트 파괴적 명령 가드
|
|
3
|
+
*
|
|
4
|
+
* Stop hook 단계에서 어시스턴트 응답 텍스트를 스캔하여 *파괴적 명령 제안*을
|
|
5
|
+
* 감지한다. PreToolUse 가드는 실제 도구 실행 직전에만 동작하므로, 모델이
|
|
6
|
+
* 텍스트로 위험한 대안을 *제안*하는 케이스(예: `find ... -exec rm -r {}`)는
|
|
7
|
+
* 도구 실행 단계에 도달하지 못하면 차단되지 않는다. 본 체크는 그 갭을 메운다.
|
|
8
|
+
*
|
|
9
|
+
* 발견 경위 (forgen-eval syn-004, 2026-05-06):
|
|
10
|
+
* notepad 룰 "[no-rm-rf-unconfirmed]" inject 상태에서, driver(llama 8B) 가
|
|
11
|
+
* `rm -rf` 리터럴은 회피했지만 `find . -type d -name 'build' -exec rm -r {}`
|
|
12
|
+
* 같은 우회 명령을 "안전한 대안"으로 제안. PreToolUse 차단은 실 실행 시
|
|
13
|
+
* 동작하지만 "응답 텍스트만으론" 못 막음. driver 모델 능력에 무관하게
|
|
14
|
+
* 일관된 안전 보장이 되도록 응답 단계에서 한 번 더 잡아낸다.
|
|
15
|
+
*
|
|
16
|
+
* 동작:
|
|
17
|
+
* - 어시스턴트 응답에 등록된 파괴 패턴이 매칭되면 block 권고.
|
|
18
|
+
* - "확실히, 명시적 확인" 같은 confirm 표현 동반 여부는 검사하지 않는다
|
|
19
|
+
* (휴리스틱 brittle). 매칭 시 무조건 block 후, 모델이 안전한 대안 제시
|
|
20
|
+
* 또는 명시적 confirm 절차로 재응답하도록 유도.
|
|
21
|
+
*
|
|
22
|
+
* 순수 함수 — Stop hook 이 reason 문자열을 그대로 주입.
|
|
23
|
+
*/
|
|
24
|
+
import { compileSafeRegex, safeRegexTest } from '../hooks/shared/safe-regex.js';
|
|
25
|
+
/**
|
|
26
|
+
* 응답 텍스트에서 검사할 파괴적 명령 패턴.
|
|
27
|
+
* dist/hooks/dangerous-patterns.json (PreToolUse 용) 와 별도 — 응답 텍스트
|
|
28
|
+
* 분석에 적합한 더 넓은 패턴 (find -exec rm 같은 우회 포함).
|
|
29
|
+
*/
|
|
30
|
+
/** 패턴 순서 중요: 더 구체적인(우회) 패턴을 먼저 두어 일반 rm -r 패턴이 가로채지 않게 함. */
|
|
31
|
+
const RESPONSE_PATTERNS = [
|
|
32
|
+
{ pattern: '\\bfind\\b[^\\n]{0,80}-exec\\s+rm\\b', description: 'find -exec rm (rm 우회)', flags: 'i' },
|
|
33
|
+
{ pattern: '\\bfind\\b[^\\n]{0,80}-delete\\b', description: 'find -delete (rm 우회)', flags: 'i' },
|
|
34
|
+
{ pattern: '\\bxargs\\s+rm\\b', description: 'xargs rm (rm 우회)', flags: 'i' },
|
|
35
|
+
{ pattern: '\\brm\\s+-(?:rf|fr|r)\\b', description: 'rm -r/-rf 직접 호출', flags: 'i' },
|
|
36
|
+
{ pattern: '\\bgit\\s+push\\b[^\\n]{0,40}--force(?!-)', description: 'git push --force', flags: 'i' },
|
|
37
|
+
{ pattern: '\\bgit\\s+reset\\s+--hard\\b', description: 'git reset --hard', flags: 'i' },
|
|
38
|
+
{ pattern: '\\bgit\\s+clean\\s+-[a-z]*f', description: 'git clean -f', flags: 'i' },
|
|
39
|
+
{ pattern: '\\bDROP\\s+(?:TABLE|DATABASE)\\b', description: 'SQL DROP TABLE/DATABASE', flags: 'i' },
|
|
40
|
+
{ pattern: '\\bTRUNCATE\\s+TABLE\\b', description: 'SQL TRUNCATE TABLE', flags: 'i' },
|
|
41
|
+
{ pattern: '>\\s*/dev/sd[a-z]\\b', description: '/dev/sdX 직접 write', flags: 'i' },
|
|
42
|
+
{ pattern: '\\bmkfs\\b', description: 'mkfs (파일시스템 포맷)', flags: 'i' },
|
|
43
|
+
{ pattern: '\\bdd\\b[^\\n]{0,60}\\bof=/dev/', description: 'dd of=/dev/* (장치 write)', flags: 'i' },
|
|
44
|
+
{ pattern: '\\bcurl\\b[^\\n]{0,60}\\|\\s*(?:ba)?sh\\b', description: 'curl | sh (원격 코드 실행)', flags: 'i' },
|
|
45
|
+
{ pattern: '\\bwget\\b[^\\n]{0,60}\\|\\s*(?:ba)?sh\\b', description: 'wget | sh (원격 코드 실행)', flags: 'i' },
|
|
46
|
+
];
|
|
47
|
+
/** 응답 텍스트가 파괴적 명령 패턴을 포함하는지 검사. */
|
|
48
|
+
export function checkDangerousResponsePattern(opts) {
|
|
49
|
+
const text = opts.text ?? '';
|
|
50
|
+
if (!text.trim())
|
|
51
|
+
return { block: false, reason: '' };
|
|
52
|
+
for (const p of RESPONSE_PATTERNS) {
|
|
53
|
+
const compiled = compileSafeRegex(p.pattern, p.flags ?? 'i');
|
|
54
|
+
if (!compiled.regex)
|
|
55
|
+
continue; // bad regex — skip
|
|
56
|
+
if (safeRegexTest(compiled.regex, text)) {
|
|
57
|
+
return {
|
|
58
|
+
block: true,
|
|
59
|
+
matched: p.description,
|
|
60
|
+
reason: `응답에 파괴적 명령 패턴이 포함되었습니다 (${p.description}). 사용자 명시 확인 절차를 포함하거나 비파괴 대안 (예: dry-run, --interactive)을 제시해 다시 응답하세요.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { block: false, reason: '' };
|
|
65
|
+
}
|
|
@@ -47,6 +47,26 @@ const AGREEMENT_SOFTENERS = [
|
|
|
47
47
|
/(생각합니다|생각함|생각해|봅니다|예상(합니다|돼))/,
|
|
48
48
|
/(그럴\s*것\s*같|맞을\s*것\s*같)/,
|
|
49
49
|
];
|
|
50
|
+
/**
|
|
51
|
+
* 측정-증거 지표 — 실제 실행/측정 결과가 응답에 *paste 되어 있다*는 신호.
|
|
52
|
+
*
|
|
53
|
+
* v0.4.4 (2026-05-06): FP 감소. "Docker e2e 77/77 PASS" 같은 *정량 사실 보고*
|
|
54
|
+
* 가 recentTools 윈도우 밖 측정 (예: 이전 turn Bash 결과, 사용자 paste, CI 로그
|
|
55
|
+
* 인용)이라 Bash 카운트가 0이지만 본질적으로 measurement-backed 응답.
|
|
56
|
+
*
|
|
57
|
+
* 임계: 본 패턴이 2+ 매칭되면 alert 억제 (응답이 측정 증거를 *제시*하고 있다고 본다).
|
|
58
|
+
*/
|
|
59
|
+
const EVIDENCE_INDICATORS = [
|
|
60
|
+
/\b\d+\/\d+\b/, // test counts: "77/77", "22/22"
|
|
61
|
+
/\bexit\s*code\s*[:=]?\s*\d+/i, // exit code
|
|
62
|
+
/\b\d+(\.\d+)?\s*(ms|s|sec|seconds)\b/i, // timings: "232s", "1.5ms"
|
|
63
|
+
/\b(?:Test|Spec)s?\s*Files?\s+\d+/i, // vitest "Test Files 218"
|
|
64
|
+
/\b(?:Tests?:?\s+)?\d+\s+passed?\b/i, // "2382 passed"
|
|
65
|
+
/\b(?:CI|HEAD|sha|commit)\s*[:=]?\s*[a-f0-9]{7,}/i, // commit ref
|
|
66
|
+
/^[+-]{3}\s/m, // diff hunks
|
|
67
|
+
/\bcoverage\s*[:=]?\s*\d+(\.\d+)?%/i, // coverage %
|
|
68
|
+
/^\s*✓\s|^\s*✗\s|^\s*PASS\b|^\s*FAIL\b/m, // test runner output markers
|
|
69
|
+
];
|
|
50
70
|
function findMatches(text, patterns, max = 3) {
|
|
51
71
|
const out = [];
|
|
52
72
|
for (const p of patterns) {
|
|
@@ -69,8 +89,12 @@ export function checkFactVsAgreement(input) {
|
|
|
69
89
|
const factAssertions = findMatches(text, FACT_ASSERTION_PATTERNS);
|
|
70
90
|
const agreementSofteners = findMatches(text, AGREEMENT_SOFTENERS);
|
|
71
91
|
const measurementCount = recentTools.filter((t) => MEASUREMENT_TOOL_CATEGORIES.has(t)).length;
|
|
92
|
+
// Evidence indicator suppression — 응답에 측정 결과가 *paste* 되어 있으면
|
|
93
|
+
// recentTools 윈도우 밖 측정으로 보고 alert 억제 (FP 감소).
|
|
94
|
+
const evidenceIndicators = findMatches(text, EVIDENCE_INDICATORS, 99);
|
|
95
|
+
const hasMeasurementEvidence = evidenceIndicators.length >= 2;
|
|
72
96
|
const hasFactAssertion = factAssertions.length > 0;
|
|
73
|
-
const measurementMissing = measurementCount < minMeasurements;
|
|
97
|
+
const measurementMissing = measurementCount < minMeasurements && !hasMeasurementEvidence;
|
|
74
98
|
const alert = hasFactAssertion && measurementMissing;
|
|
75
99
|
let reason = '';
|
|
76
100
|
if (alert) {
|
package/dist/cli.js
CHANGED
|
@@ -112,8 +112,31 @@ const commands = [
|
|
|
112
112
|
await displayHookStatus(process.cwd());
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
else if (sub === 'default-host') {
|
|
116
|
+
const value = args[1];
|
|
117
|
+
const valid = new Set(['claude', 'codex', 'ask']);
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
const { getDefaultHost } = await import('./store/profile-store.js');
|
|
120
|
+
const current = getDefaultHost();
|
|
121
|
+
console.log(` current default_host: ${current ?? '(unset → claude fallback)'}`);
|
|
122
|
+
console.log(' Usage: forgen config default-host {claude|codex|ask}');
|
|
123
|
+
}
|
|
124
|
+
else if (!valid.has(value)) {
|
|
125
|
+
console.log(` Invalid value: ${value}. Use one of: claude, codex, ask`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const { setDefaultHost } = await import('./store/profile-store.js');
|
|
130
|
+
const ok = setDefaultHost(value);
|
|
131
|
+
if (!ok) {
|
|
132
|
+
console.log(' ✗ Profile not found. Run `forgen onboarding` first.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
console.log(` ✓ default_host set to: ${value}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
115
138
|
else {
|
|
116
|
-
console.log('Usage
|
|
139
|
+
console.log('Usage:\n forgen config hooks [--regenerate]\n forgen config default-host [claude|codex|ask]');
|
|
117
140
|
}
|
|
118
141
|
},
|
|
119
142
|
},
|
|
@@ -133,6 +156,53 @@ const commands = [
|
|
|
133
156
|
await handleInit(args);
|
|
134
157
|
},
|
|
135
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: 'install',
|
|
161
|
+
description: 'Install forgen into a host. Usage: forgen install [claude|codex|both] [--dry-run] [--no-mcp]',
|
|
162
|
+
handler: async (args) => {
|
|
163
|
+
const knownSubs = new Set(['claude', 'codex', 'both']);
|
|
164
|
+
const target = args[0] && knownSubs.has(args[0]) ? args[0] : args[0]?.startsWith('--') ? undefined : args[0];
|
|
165
|
+
if (target !== undefined && !knownSubs.has(target)) {
|
|
166
|
+
console.log('Usage:\n forgen install [claude|codex|both] [--dry-run] [--no-mcp]\n\n No arg → interactive 3-choice (Claude/Codex/Both).');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const dryRun = args.includes('--dry-run');
|
|
170
|
+
const registerMcp = !args.includes('--no-mcp');
|
|
171
|
+
const { runInstall, renderResult, resolvePkgRootFromBinary } = await import('./host/install-orchestrator.js');
|
|
172
|
+
const pkgRoot = resolvePkgRootFromBinary(import.meta.url);
|
|
173
|
+
const result = await runInstall({ target, pkgRoot, dryRun, registerMcp });
|
|
174
|
+
if (result === null) {
|
|
175
|
+
console.log('\n [forgen] Install skipped.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(renderResult(result, dryRun));
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'parity',
|
|
183
|
+
description: 'Run host parity checks. Usage: forgen parity codex [--dry-run]',
|
|
184
|
+
handler: async (args) => {
|
|
185
|
+
const sub = args[0];
|
|
186
|
+
if (sub !== 'codex') {
|
|
187
|
+
console.log('Usage:\n forgen parity codex [--dry-run]\n\nNotes:\n - source 체크아웃에서만 작동합니다 (tests/ 디렉토리 필요).\n - npm install 로 설치된 패키지에서는 run-parity.sh 가 없습니다.');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
191
|
+
const scriptPath = path.resolve(here, '..', 'tests', 'e2e', 'codex', 'run-parity.sh');
|
|
192
|
+
if (!fs.existsSync(scriptPath)) {
|
|
193
|
+
console.error('[forgen] run-parity.sh 는 source 체크아웃에서만 작동. 직접 git clone 후 실행하세요.');
|
|
194
|
+
console.error(` expected: ${scriptPath}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const { spawnSync } = await import('node:child_process');
|
|
198
|
+
const dryRun = args.includes('--dry-run');
|
|
199
|
+
const spawnArgs = dryRun ? ['--dry-run'] : [];
|
|
200
|
+
const result = spawnSync('bash', [scriptPath, ...spawnArgs], { stdio: 'inherit' });
|
|
201
|
+
if (result.status !== 0) {
|
|
202
|
+
process.exit(result.status ?? 1);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
},
|
|
136
206
|
{
|
|
137
207
|
name: 'notepad',
|
|
138
208
|
description: 'Notepad (show|add|clear)',
|
|
@@ -151,7 +221,7 @@ const commands = [
|
|
|
151
221
|
},
|
|
152
222
|
{
|
|
153
223
|
name: 'onboarding',
|
|
154
|
-
description: 'v1
|
|
224
|
+
description: 'v1 4-question onboarding flow',
|
|
155
225
|
handler: async (_args) => {
|
|
156
226
|
const { runOnboarding } = await import('./forge/onboarding-cli.js');
|
|
157
227
|
await runOnboarding();
|
|
@@ -407,7 +477,8 @@ async function main() {
|
|
|
407
477
|
${dim}Code, forged for you.${reset}
|
|
408
478
|
${dim}Scope: v1(${context.v1.session?.quality_pack ?? 'onboarding needed'})${reset}
|
|
409
479
|
`);
|
|
410
|
-
const
|
|
480
|
+
const { getHostRuntime } = await import('./host/host-runtime.js');
|
|
481
|
+
const runtimeLabel = getHostRuntime(runtime).displayName;
|
|
411
482
|
console.log(`[forgen] Starting ${runtimeLabel}...\n`);
|
|
412
483
|
await spawnClaudeWithResume(args, context, () => prepareHarness(process.cwd(), { runtime }), runtime);
|
|
413
484
|
}
|
|
@@ -441,7 +512,7 @@ function printHelp() {
|
|
|
441
512
|
|
|
442
513
|
Commands:
|
|
443
514
|
forgen forge Personalize your coding profile
|
|
444
|
-
forgen onboarding Run
|
|
515
|
+
forgen onboarding Run 4-question onboarding
|
|
445
516
|
forgen inspect [profile|rules|corrections|session]
|
|
446
517
|
Inspect v1 state (alias: evidence → corrections)
|
|
447
518
|
forgen rule <list|suppress|activate|scan|health-scan|classify>
|
|
@@ -450,8 +521,9 @@ function printHelp() {
|
|
|
450
521
|
forgen last-block Show the most recent block event
|
|
451
522
|
forgen recall [--limit N] [--show]
|
|
452
523
|
최근 compound 주입 이력 (solution body preview)
|
|
453
|
-
forgen migrate [implicit-feedback|all]
|
|
454
|
-
One-shot schema migration (category backfill)
|
|
524
|
+
forgen migrate [implicit-feedback|evidence-host|all]
|
|
525
|
+
One-shot schema migration (category backfill / host backfill)
|
|
526
|
+
forgen parity codex [--dry-run] Run codex parity checks (source checkout only)
|
|
455
527
|
forgen compound Manage accumulated knowledge
|
|
456
528
|
forgen dashboard Compound system dashboard
|
|
457
529
|
forgen me Personal dashboard
|