@wooojin/forgen 0.4.0 → 0.4.3
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 +194 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +74 -9
- package/README.ko.md +77 -12
- package/README.md +127 -25
- package/README.zh.md +43 -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/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/conclusion-verification-ratio.d.ts +37 -0
- package/dist/checks/conclusion-verification-ratio.js +86 -0
- package/dist/checks/fact-vs-agreement.d.ts +47 -0
- package/dist/checks/fact-vs-agreement.js +92 -0
- package/dist/checks/self-score-deflation.d.ts +38 -0
- package/dist/checks/self-score-deflation.js +108 -0
- package/dist/cli.js +98 -6
- package/dist/core/auto-compound-runner.js +137 -49
- 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 +41 -2
- package/dist/core/doctor.js +118 -5
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -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/init-cli.d.ts +26 -0
- package/dist/core/init-cli.js +104 -0
- package/dist/core/init.js +17 -0
- package/dist/core/inspect-cli.js +1 -2
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +11 -0
- package/dist/core/migrate-cli.js +53 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/paths.d.ts +8 -1
- package/dist/core/paths.js +11 -2
- package/dist/core/recall-cli.d.ts +26 -0
- package/dist/core/recall-cli.js +125 -0
- package/dist/core/recall-reference-detector.d.ts +43 -0
- package/dist/core/recall-reference-detector.js +65 -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.d.ts +21 -0
- package/dist/core/stats-cli.js +133 -10
- 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/core/uninstall.js +2 -1
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +5 -6
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
- package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
- package/dist/engine/lifecycle/orchestrator.js +2 -2
- package/dist/engine/lifecycle/signals.js +6 -6
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
- package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
- package/dist/engine/skill-promoter.js +3 -6
- 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/context-guard.js +1 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +21 -5
- 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/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +2 -2
- package/dist/hooks/notepad-injector.js +1 -1
- package/dist/hooks/permission-handler.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +7 -1
- package/dist/hooks/post-tool-use.js +50 -23
- package/dist/hooks/pre-compact.js +2 -2
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +28 -10
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +1 -1
- package/dist/hooks/session-recovery.js +12 -1
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -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 +30 -2
- package/dist/hooks/shared/hook-response.js +61 -3
- package/dist/hooks/skill-injector.js +2 -2
- package/dist/hooks/slop-detector.js +2 -2
- package/dist/hooks/solution-injector.d.ts +9 -0
- package/dist/hooks/solution-injector.js +48 -5
- package/dist/hooks/stop-guard.js +152 -13
- package/dist/hooks/subagent-tracker.js +1 -1
- 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/i18n/index.js +3 -5
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +45 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/rule-store.js +8 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +7 -5
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /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}/calibrate.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}/retro.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
|
@@ -20,6 +20,8 @@ import { approve, approveWithWarning, failOpenWithTracking } from './shared/hook
|
|
|
20
20
|
import { STATE_DIR } from '../core/paths.js';
|
|
21
21
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
22
22
|
import { createDriftState, evaluateDrift } from '../core/drift-score.js';
|
|
23
|
+
import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
|
|
24
|
+
const RECENT_TOOL_NAMES_WINDOW = 20;
|
|
23
25
|
/** Lightweight hash for content comparison (not cryptographic) */
|
|
24
26
|
function simpleHash(content) {
|
|
25
27
|
let hash = 0;
|
|
@@ -30,15 +32,6 @@ function simpleHash(content) {
|
|
|
30
32
|
}
|
|
31
33
|
return hash.toString(36);
|
|
32
34
|
}
|
|
33
|
-
const IMPLICIT_FEEDBACK_LOG = path.join(STATE_DIR, 'implicit-feedback.jsonl');
|
|
34
|
-
/** Record implicit feedback signal to JSONL */
|
|
35
|
-
function recordImplicitFeedback(entry) {
|
|
36
|
-
try {
|
|
37
|
-
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
38
|
-
fs.appendFileSync(IMPLICIT_FEEDBACK_LOG, JSON.stringify(entry) + '\n');
|
|
39
|
-
}
|
|
40
|
-
catch { /* fail-open: implicit feedback recording must not throw */ }
|
|
41
|
-
}
|
|
42
35
|
// ── State management ──
|
|
43
36
|
function getModifiedFilesPath(sessionId) {
|
|
44
37
|
return path.join(STATE_DIR, `modified-files-${sanitizeId(sessionId)}.json`);
|
|
@@ -82,15 +75,21 @@ const AGENT_QUALITY_PATTERNS = [
|
|
|
82
75
|
{ pattern: /(?:context (?:window|limit) (?:exceeded|reached)|too (?:large|long) to (?:read|process))/i, signal: 'agent_context_overflow', severity: 'warning', message: 'Agent hit context limits — output may be incomplete' },
|
|
83
76
|
];
|
|
84
77
|
export function validateAgentOutput(toolResponse) {
|
|
85
|
-
|
|
78
|
+
// tool_response 는 string / object / array 모두 가능. main() 측에서 stringify 를 한 번 더
|
|
79
|
+
// 하지만 직접 호출 보호 (defense in depth).
|
|
80
|
+
if (typeof toolResponse !== 'string') {
|
|
81
|
+
toolResponse = toolResponse == null ? '' : JSON.stringify(toolResponse);
|
|
82
|
+
}
|
|
83
|
+
const r = toolResponse;
|
|
84
|
+
if (!r || r.trim().length < AGENT_MIN_OUTPUT_LENGTH) {
|
|
86
85
|
return {
|
|
87
86
|
signal: 'agent_empty_output',
|
|
88
87
|
severity: 'warning',
|
|
89
|
-
message: `Agent returned minimal output (${
|
|
88
|
+
message: `Agent returned minimal output (${r.trim().length} chars). Verify the result is usable.`,
|
|
90
89
|
};
|
|
91
90
|
}
|
|
92
91
|
for (const p of AGENT_QUALITY_PATTERNS) {
|
|
93
|
-
if (p.pattern.test(
|
|
92
|
+
if (p.pattern.test(r)) {
|
|
94
93
|
return { signal: p.signal, severity: p.severity, message: p.message };
|
|
95
94
|
}
|
|
96
95
|
}
|
|
@@ -121,10 +120,22 @@ async function main() {
|
|
|
121
120
|
}
|
|
122
121
|
const toolName = data.tool_name ?? data.toolName ?? '';
|
|
123
122
|
const toolInput = data.tool_input ?? data.toolInput ?? {};
|
|
124
|
-
|
|
123
|
+
// tool_response 는 string / object / array 모두 가능 (sub-agent 결과는 object 가 흔함).
|
|
124
|
+
// 모든 downstream 이 string 가정이라 stringify 로 normalize. 회귀 박제: tests/hooks/post-tool-use.test.ts
|
|
125
|
+
const rawResponse = data.tool_response ?? data.toolOutput ?? '';
|
|
126
|
+
const toolResponse = typeof rawResponse === 'string' ? rawResponse : JSON.stringify(rawResponse);
|
|
125
127
|
const sessionId = data.session_id ?? 'default';
|
|
126
128
|
const modState = loadModifiedFiles(sessionId);
|
|
127
129
|
modState.toolCallCount = (modState.toolCallCount ?? 0) + 1;
|
|
130
|
+
// TEST-2: recent tool name window — stop-guard 의 self-score inflation 가드가
|
|
131
|
+
// "최근 세션에서 측정 도구 몇 번 불렸나?" 를 이 배열로 계산한다.
|
|
132
|
+
if (toolName) {
|
|
133
|
+
const names = modState.recentToolNames ?? [];
|
|
134
|
+
names.push(toolName);
|
|
135
|
+
if (names.length > RECENT_TOOL_NAMES_WINDOW)
|
|
136
|
+
names.splice(0, names.length - RECENT_TOOL_NAMES_WINDOW);
|
|
137
|
+
modState.recentToolNames = names;
|
|
138
|
+
}
|
|
128
139
|
const messages = [];
|
|
129
140
|
let revertDetected = false;
|
|
130
141
|
// 1. Checkpoint (every 5 calls)
|
|
@@ -152,8 +163,9 @@ async function main() {
|
|
|
152
163
|
// Implicit feedback: repeated edit detection (5+ edits on same file)
|
|
153
164
|
if (count >= 5) {
|
|
154
165
|
messages.push(`<compound-tool-warning>\n[Forgen] ⚠ ${path.basename(filePath)} has been modified ${count} times.\nConsider redesigning the overall structure and restarting.\n</compound-tool-warning>`);
|
|
155
|
-
|
|
166
|
+
appendImplicitFeedback({
|
|
156
167
|
type: 'repeated_edit',
|
|
168
|
+
category: 'edit',
|
|
157
169
|
file: filePath,
|
|
158
170
|
editCount: count,
|
|
159
171
|
at: new Date().toISOString(),
|
|
@@ -172,8 +184,9 @@ async function main() {
|
|
|
172
184
|
// Skip the most recent hash (which would be the write being "reverted from")
|
|
173
185
|
if (prevHashes.length >= 2 && prevHashes.slice(0, -1).includes(hash)) {
|
|
174
186
|
revertDetected = true;
|
|
175
|
-
|
|
187
|
+
appendImplicitFeedback({
|
|
176
188
|
type: 'revert_detected',
|
|
189
|
+
category: 'revert',
|
|
177
190
|
file: filePath,
|
|
178
191
|
at: new Date().toISOString(),
|
|
179
192
|
sessionId,
|
|
@@ -198,8 +211,9 @@ async function main() {
|
|
|
198
211
|
const driftResult = evaluateDrift(modState.drift, true, revertDetected);
|
|
199
212
|
if (driftResult.message) {
|
|
200
213
|
messages.push(`<compound-tool-warning>\n${driftResult.message}\n</compound-tool-warning>`);
|
|
201
|
-
|
|
214
|
+
appendImplicitFeedback({
|
|
202
215
|
type: driftResult.level === 'critical' || driftResult.level === 'hardcap' ? 'drift_critical' : 'drift_warning',
|
|
216
|
+
category: 'drift',
|
|
203
217
|
score: driftResult.score,
|
|
204
218
|
totalEdits: modState.drift.totalEdits,
|
|
205
219
|
totalReverts: modState.drift.totalReverts,
|
|
@@ -213,8 +227,9 @@ async function main() {
|
|
|
213
227
|
const agentResult = validateAgentOutput(toolResponse);
|
|
214
228
|
if (agentResult) {
|
|
215
229
|
messages.push(`<compound-agent-validation>\n[Forgen] ${agentResult.severity === 'error' ? '⛔' : '⚠'} ${agentResult.message}\n</compound-agent-validation>`);
|
|
216
|
-
|
|
230
|
+
appendImplicitFeedback({
|
|
217
231
|
type: `agent_${agentResult.signal}`,
|
|
232
|
+
category: 'agent',
|
|
218
233
|
severity: agentResult.severity,
|
|
219
234
|
outputLength: toolResponse.trim().length,
|
|
220
235
|
at: new Date().toISOString(),
|
|
@@ -254,14 +269,18 @@ async function main() {
|
|
|
254
269
|
})() || toolResponse;
|
|
255
270
|
if (target) {
|
|
256
271
|
try {
|
|
257
|
-
const [{ loadActiveRules }, { recordViolation, recordBypass }, { scanForBypass }, { compileSafeRegex, safeRegexTest },] = await Promise.all([
|
|
272
|
+
const [{ loadActiveRules }, { recordViolation, recordBypass }, { scanForBypass }, { compileSafeRegex, safeRegexTest }, { preprocessForMatch },] = await Promise.all([
|
|
258
273
|
import('../store/rule-store.js'),
|
|
259
274
|
import('../engine/lifecycle/signals.js'),
|
|
260
275
|
import('../engine/lifecycle/bypass-detector.js'),
|
|
261
276
|
import('./shared/safe-regex.js'),
|
|
277
|
+
import('./shared/command-parser.js'),
|
|
262
278
|
]);
|
|
263
279
|
const rules = loadActiveRules();
|
|
264
|
-
// Mech-A pattern_match dispatcher
|
|
280
|
+
// Mech-A pattern_match dispatcher — match_target 은 **rule-per-rule**.
|
|
281
|
+
// AWS key / DROP 류 secret/dangerous SQL 은 파일 content 에 들어있어도
|
|
282
|
+
// 실제 leak 이라 raw 검사가 맞고, rm -rf 류 shell 명령은 quote 안 본문이면
|
|
283
|
+
// false-positive 이므로 masked 가 맞다. pre-tool-use 와 동일한 spec 기반 분기.
|
|
265
284
|
for (const rule of rules) {
|
|
266
285
|
for (const spec of rule.enforce_via ?? []) {
|
|
267
286
|
if (spec.hook !== 'PostToolUse' || spec.mech !== 'A')
|
|
@@ -277,7 +296,9 @@ async function main() {
|
|
|
277
296
|
log.debug(`rule ${rule.rule_id} unsafe regex: ${re.reason}`);
|
|
278
297
|
continue;
|
|
279
298
|
}
|
|
280
|
-
|
|
299
|
+
const matchTarget = (v.params?.match_target ?? 'raw');
|
|
300
|
+
const mechTarget = preprocessForMatch(target, matchTarget);
|
|
301
|
+
if (!safeRegexTest(re.regex, mechTarget))
|
|
281
302
|
continue;
|
|
282
303
|
recordViolation({
|
|
283
304
|
rule_id: rule.rule_id, session_id: sessionId,
|
|
@@ -288,8 +309,14 @@ async function main() {
|
|
|
288
309
|
messages.push(`<compound-rule-violation>\n[Forgen] Rule ${rule.rule_id.slice(0, 8)} pattern matched in ${toolName} output.\n${spec.block_message ?? rule.policy.slice(0, 120)}\n</compound-rule-violation>`);
|
|
289
310
|
}
|
|
290
311
|
}
|
|
291
|
-
// T3 bypass detection
|
|
292
|
-
|
|
312
|
+
// T3 bypass detection — scanForBypass 는 rule.policy 자연어에서 패턴 추출이라
|
|
313
|
+
// match_target 개념 없음. Write/Edit 는 파일 본문이라 bypass-detector 의
|
|
314
|
+
// 자연어 휴리스틱이 false-positive 과다 (L1-no-rm-rf-unconfirmed bypass 20건
|
|
315
|
+
// 중 Write/Edit 15건이 실측). 이 경로만 masked. Bash 는 실제 실행된 명령이라
|
|
316
|
+
// raw 유지. Mech-A pattern_match 는 위에서 rule-per-rule 로 이미 처리.
|
|
317
|
+
const isFileContentTool = toolName === 'Write' || toolName === 'Edit';
|
|
318
|
+
const bypassTarget = isFileContentTool ? preprocessForMatch(target, 'masked') : target;
|
|
319
|
+
const candidates = scanForBypass({ rules, tool_name: toolName, tool_output: bypassTarget, session_id: sessionId });
|
|
293
320
|
for (const c of candidates) {
|
|
294
321
|
recordBypass({ rule_id: c.rule_id, session_id: c.session_id, tool: c.tool, pattern_preview: c.pattern_preview });
|
|
295
322
|
}
|
|
@@ -322,5 +349,5 @@ async function main() {
|
|
|
322
349
|
}
|
|
323
350
|
main().catch((e) => {
|
|
324
351
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
325
|
-
console.log(failOpenWithTracking('post-tool-use'));
|
|
352
|
+
console.log(failOpenWithTracking('post-tool-use', e));
|
|
326
353
|
});
|
|
@@ -74,7 +74,7 @@ export function buildSessionBrief(sessionId) {
|
|
|
74
74
|
}
|
|
75
75
|
catch { /* fail-open */ }
|
|
76
76
|
// solutionsInjected: read injection-cache-*.json files, collect solutions[].name
|
|
77
|
-
|
|
77
|
+
const solutionsInjected = [];
|
|
78
78
|
try {
|
|
79
79
|
if (fs.existsSync(STATE_DIR)) {
|
|
80
80
|
for (const f of fs.readdirSync(STATE_DIR)) {
|
|
@@ -271,5 +271,5 @@ Rules:
|
|
|
271
271
|
}
|
|
272
272
|
main().catch((e) => {
|
|
273
273
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
274
|
-
console.log(failOpenWithTracking('pre-compact'));
|
|
274
|
+
console.log(failOpenWithTracking('pre-compact', e));
|
|
275
275
|
});
|
|
@@ -10,6 +10,13 @@ interface DangerousPatternEntry {
|
|
|
10
10
|
pattern: RegExp;
|
|
11
11
|
description: string;
|
|
12
12
|
severity: 'block' | 'warn';
|
|
13
|
+
/**
|
|
14
|
+
* match_target (v0.4.1): 'masked' (default) — quote/heredoc 본문 제거 후 매칭.
|
|
15
|
+
* 실 shell 실행 토큰 검사에 적합. 'raw' — 원본 command 그대로 매칭.
|
|
16
|
+
* `python -c "..."`, `eval "..."` 처럼 **quote 안 본문이 실제 payload 로 실행**
|
|
17
|
+
* 되는 패턴에 사용.
|
|
18
|
+
*/
|
|
19
|
+
matchTarget?: 'raw' | 'masked';
|
|
13
20
|
}
|
|
14
21
|
/** 위험 Bash 명령어 패턴 (패키지 내장 + 사용자 커스텀 병합) */
|
|
15
22
|
export declare const DANGEROUS_PATTERNS: DangerousPatternEntry[];
|
|
@@ -19,9 +19,10 @@ import { sanitizeId } from './shared/sanitize-id.js';
|
|
|
19
19
|
import { incrementEvidence } from '../engine/solution-writer.js';
|
|
20
20
|
import { isReflectionCandidate } from './compound-reflection.js';
|
|
21
21
|
import { isHookEnabled } from './hook-config.js';
|
|
22
|
-
import { approve, approveWithWarning,
|
|
22
|
+
import { approve, approveWithWarning, denyOrObserve, failOpenWithTracking } from './shared/hook-response.js';
|
|
23
23
|
import { FORGEN_HOME, STATE_DIR } from '../core/paths.js';
|
|
24
24
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
25
|
+
import { maskQuotedContent } from './shared/command-parser.js';
|
|
25
26
|
const FAIL_COUNTER_PATH = path.join(STATE_DIR, 'pre-tool-fail-counter.json');
|
|
26
27
|
const FAIL_CLOSE_THRESHOLD = 3; // 연속 3회 파싱 실패 시에만 reject
|
|
27
28
|
/** RegExp 안전성 검증 (ReDoS 방지) — 매칭/비매칭 양쪽 모두 테스트 */
|
|
@@ -59,12 +60,16 @@ function loadDangerousPatterns() {
|
|
|
59
60
|
pattern: new RegExp(entry.pattern, entry.flags ?? ''),
|
|
60
61
|
description: entry.description,
|
|
61
62
|
severity: entry.severity,
|
|
63
|
+
matchTarget: entry.match_target === 'raw' ? 'raw' : 'masked',
|
|
62
64
|
});
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
catch {
|
|
66
68
|
// JSON 로드 실패 시 하드코딩 폴백 (최소 안전장치)
|
|
67
|
-
results.push(
|
|
69
|
+
results.push(
|
|
70
|
+
// v0.4.1 false-positive fix: /tmp, /var/folders, /var/tmp 같은 임시 경로는
|
|
71
|
+
// 일반 개발에서 매일 정리 대상. 위험한 시스템 경로만 blacklist.
|
|
72
|
+
{ pattern: /rm\s+(-rf|-fr)\s+(\/(?!tmp\b|var\/folders\b|var\/tmp\b)|~)/, description: 'rm -rf on root/home path', severity: 'block' }, { pattern: /curl\s+.*\|\s*(ba)?sh/, description: 'curl pipe to shell', severity: 'block' }, { pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/, description: 'fork bomb', severity: 'block' });
|
|
68
73
|
}
|
|
69
74
|
// 2. 사용자 커스텀 패턴 (~/.compound/dangerous-patterns.json)
|
|
70
75
|
try {
|
|
@@ -100,8 +105,14 @@ export function checkDangerousCommand(toolName, toolInput) {
|
|
|
100
105
|
const command = typeof toolInput === 'string'
|
|
101
106
|
? toolInput
|
|
102
107
|
: (toolInput.command ?? '');
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
// v0.4.1 (2026-04-24) quote-aware built-in scan:
|
|
109
|
+
// 기본은 masked (quote/heredoc 본문 제거) — shell 실행 토큰 검사. 하지만
|
|
110
|
+
// `python -c "..."` / `eval "..."` 처럼 quote 안 본문이 실 payload 로 실행되는
|
|
111
|
+
// 패턴은 match_target:raw 로 지정해 원본 command 전체 검사.
|
|
112
|
+
const maskedCommand = maskQuotedContent(command);
|
|
113
|
+
for (const { pattern, description, severity, matchTarget } of DANGEROUS_PATTERNS) {
|
|
114
|
+
const target = matchTarget === 'raw' ? command : maskedCommand;
|
|
115
|
+
if (pattern.test(target)) {
|
|
105
116
|
return { action: severity, description, command: command.slice(0, 100) };
|
|
106
117
|
}
|
|
107
118
|
}
|
|
@@ -294,7 +305,7 @@ async function main() {
|
|
|
294
305
|
// for `forgen doctor` / log inspection. Mirrors `db-guard.ts:85-96`.
|
|
295
306
|
const failCount = getAndIncrementFailCount();
|
|
296
307
|
if (failCount >= FAIL_CLOSE_THRESHOLD) {
|
|
297
|
-
console.log(
|
|
308
|
+
console.log(denyOrObserve('pre-tool-use', `[Forgen] PreToolUse: stdin parse failed ${failCount} consecutive times — blocking for safety.`));
|
|
298
309
|
}
|
|
299
310
|
else {
|
|
300
311
|
process.stderr.write(`[ch-hook] pre-tool-use stdin parse failed (${failCount}/${FAIL_CLOSE_THRESHOLD})\n`);
|
|
@@ -315,10 +326,11 @@ async function main() {
|
|
|
315
326
|
// 이렇게 해야 rule.block_message (맥락 있는 안내) 가 제네릭 "Dangerous command blocked" 대신 노출됨.
|
|
316
327
|
// fail-open: 예외는 hook 차단 안 함.
|
|
317
328
|
try {
|
|
318
|
-
const [{ loadActiveRules }, { recordViolation }, { compileSafeRegex, safeRegexTest },] = await Promise.all([
|
|
329
|
+
const [{ loadActiveRules }, { recordViolation }, { compileSafeRegex, safeRegexTest }, { preprocessForMatch },] = await Promise.all([
|
|
319
330
|
import('../store/rule-store.js'),
|
|
320
331
|
import('../engine/lifecycle/signals.js'),
|
|
321
332
|
import('./shared/safe-regex.js'),
|
|
333
|
+
import('./shared/command-parser.js'),
|
|
322
334
|
]);
|
|
323
335
|
const rules = loadActiveRules();
|
|
324
336
|
const command = typeof toolInput.command === 'string'
|
|
@@ -339,7 +351,13 @@ async function main() {
|
|
|
339
351
|
log.debug(`rule ${rule.rule_id} unsafe regex: ${re.reason}`);
|
|
340
352
|
continue;
|
|
341
353
|
}
|
|
342
|
-
|
|
354
|
+
// TEST-6 / RC5: quote-aware preprocessing. Default 'raw' = backward compat.
|
|
355
|
+
// Rules that target real command invocations should set match_target: 'masked'
|
|
356
|
+
// so quoted argument text (e.g. body of `forgen compound --solution "..."`)
|
|
357
|
+
// doesn't trigger false positive blocks.
|
|
358
|
+
const matchTarget = (v.params?.match_target ?? 'raw');
|
|
359
|
+
const target = preprocessForMatch(command, matchTarget);
|
|
360
|
+
if (!safeRegexTest(re.regex, target))
|
|
343
361
|
continue;
|
|
344
362
|
const requiresFlag = v.params?.requires_flag;
|
|
345
363
|
const confirmed = process.env.FORGEN_USER_CONFIRMED === '1';
|
|
@@ -348,7 +366,7 @@ async function main() {
|
|
|
348
366
|
const baseMsg = spec.block_message ?? `[${rule.rule_id}] policy violation: ${rule.policy.slice(0, 120)}`;
|
|
349
367
|
// G8: override 힌트 — FORGEN_USER_CONFIRMED=1 으로 사용자 명시 승인 가능, 감사 로그 기록됨.
|
|
350
368
|
const msgWithHint = `${baseMsg}\n\n(override: set FORGEN_USER_CONFIRMED=1 (bypass will be audited in violations.jsonl))`;
|
|
351
|
-
console.log(
|
|
369
|
+
console.log(denyOrObserve('pre-tool-use', msgWithHint));
|
|
352
370
|
return;
|
|
353
371
|
}
|
|
354
372
|
if (requiresFlag && confirmed) {
|
|
@@ -370,7 +388,7 @@ async function main() {
|
|
|
370
388
|
// Bash 도구: 위험 명령어 감지 (빌트인 safety net)
|
|
371
389
|
const check = checkDangerousCommand(toolName, toolInput);
|
|
372
390
|
if (check.action === 'block') {
|
|
373
|
-
console.log(
|
|
391
|
+
console.log(denyOrObserve('pre-tool-use', `[Forgen] Dangerous command blocked: ${check.description}\nCommand: ${check.command}`));
|
|
374
392
|
return;
|
|
375
393
|
}
|
|
376
394
|
if (check.action === 'warn') {
|
|
@@ -413,5 +431,5 @@ main().catch((e) => {
|
|
|
413
431
|
});
|
|
414
432
|
process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
|
|
415
433
|
// fail-open: approve on internal error to avoid blocking all tool calls
|
|
416
|
-
console.log(failOpenWithTracking('pre-tool-use'));
|
|
434
|
+
console.log(failOpenWithTracking('pre-tool-use', e));
|
|
417
435
|
});
|
|
@@ -10,7 +10,7 @@ import * as path from 'node:path';
|
|
|
10
10
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
11
11
|
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
12
12
|
import { isHookEnabled } from './hook-config.js';
|
|
13
|
-
import { approve,
|
|
13
|
+
import { approve, denyOrObserve, failOpenWithTracking } from './shared/hook-response.js';
|
|
14
14
|
import { STATE_DIR } from '../core/paths.js';
|
|
15
15
|
const RATE_LIMIT_PATH = path.join(STATE_DIR, 'rate-limit.json');
|
|
16
16
|
const DEFAULT_LIMIT = 30; // calls per minute
|
|
@@ -75,12 +75,12 @@ async function main() {
|
|
|
75
75
|
saveRateLimitState(updatedState);
|
|
76
76
|
}
|
|
77
77
|
if (exceeded) {
|
|
78
|
-
console.log(
|
|
78
|
+
console.log(denyOrObserve('rate-limiter', `[Forgen] Rate limit exceeded (${count}/${DEFAULT_LIMIT}/min). Wait before retrying.`));
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
console.log(approve());
|
|
82
82
|
}
|
|
83
83
|
main().catch((e) => {
|
|
84
84
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
85
|
-
console.log(failOpenWithTracking('rate-limiter'));
|
|
85
|
+
console.log(failOpenWithTracking('rate-limiter', e));
|
|
86
86
|
});
|
|
@@ -87,5 +87,5 @@ main().catch((e) => {
|
|
|
87
87
|
hookName: 'secret-filter', eventType: 'PostToolUse', cause: e,
|
|
88
88
|
});
|
|
89
89
|
process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
|
|
90
|
-
console.log(failOpenWithTracking('secret-filter'));
|
|
90
|
+
console.log(failOpenWithTracking('secret-filter', e));
|
|
91
91
|
});
|
|
@@ -320,6 +320,17 @@ async function main() {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
catch { /* fail-open */ }
|
|
323
|
+
// US-M1 (RC6 가드): 직전 forge-loop findings 또는 진행 중 stories 자동 inject.
|
|
324
|
+
// 본 세션 자기증거 — head -80 truncation 으로 findings 누락 → 같은 가설 재발.
|
|
325
|
+
try {
|
|
326
|
+
const { readForgeLoopState, renderForgeLoopForSession } = await import('./shared/forge-loop-state.js');
|
|
327
|
+
const block = renderForgeLoopForSession(readForgeLoopState());
|
|
328
|
+
if (block)
|
|
329
|
+
recoveryMessages.push(block);
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
log.debug('forge-loop findings inject 실패', e);
|
|
333
|
+
}
|
|
323
334
|
const sessionId = sessionContext.sessionId;
|
|
324
335
|
// 이전 세션 자동 compound (fire-and-forget)
|
|
325
336
|
// /new로 세션 리셋 시 SessionStart가 다시 호출됨 — 이때 이전 transcript를 compound
|
|
@@ -429,6 +440,6 @@ async function main() {
|
|
|
429
440
|
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
430
441
|
main().catch((e) => {
|
|
431
442
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
432
|
-
console.log(failOpenWithTracking('session-recovery'));
|
|
443
|
+
console.log(failOpenWithTracking('session-recovery', e));
|
|
433
444
|
});
|
|
434
445
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocking ALLOW-LIST — P3' (2026-04-27)
|
|
3
|
+
*
|
|
4
|
+
* 사용자 작업을 차단(block)할 권한을 가진 hook 의 명시적 화이트리스트.
|
|
5
|
+
* 목록 외 hook 의 부정적 판정은 "관찰 신호"(log only) 로만 처리되어야 한다.
|
|
6
|
+
*
|
|
7
|
+
* RC5 (retro-v040): 분산된 detector 가 각자 block 결정을 내리면서 false-positive
|
|
8
|
+
* 가 메인 로직 흐름까지 차단하는 회귀 패턴 발생. ALLOW-LIST 명시화로 차단 권한
|
|
9
|
+
* 의 source-of-truth 를 단일화.
|
|
10
|
+
*
|
|
11
|
+
* v0.4.2 정책:
|
|
12
|
+
* - 본 모듈은 ALLOW-LIST 정의 + 검증 helper. 기존 deny() 직접 호출 hook 들은
|
|
13
|
+
* v0.4.2 에서 denyOrObserve(name, reason) 로 마이그레이션 완료.
|
|
14
|
+
* - 신규 hook 추가 시 차단 권한이 필요하면 본 ALLOW-LIST 에 추가 + 본 파일의
|
|
15
|
+
* 사유 문서화 의무. 본 commit diff 가 review 필수 항목.
|
|
16
|
+
*
|
|
17
|
+
* 멤버 사유:
|
|
18
|
+
* - stop-guard: Stop hook — false-completion 메타 가드 (자가 검증 강제)
|
|
19
|
+
* - pre-tool-use: Bash dangerous-pattern + 수동 confirm 가드
|
|
20
|
+
* - secret-filter: Write/Edit 결과의 .env / API key 노출 차단
|
|
21
|
+
* - db-guard: Bash 의 destructive DB 명령 (DROP/TRUNCATE/DELETE) 차단
|
|
22
|
+
* - rate-limiter: 사용자 작업 빈도 임계 초과 시 cool-down 차단 (resource abuse 방어)
|
|
23
|
+
*/
|
|
24
|
+
export declare const BLOCKING_ALLOWLIST: ReadonlySet<string>;
|
|
25
|
+
/** hook 이 block 결정을 출력할 권한이 있는지. */
|
|
26
|
+
export declare function canBlock(hookName: string): boolean;
|
|
27
|
+
/** ALLOW-LIST 에 추가하려는 hook 이 정책 문서화를 요구하는지 (lint helper). */
|
|
28
|
+
export declare function requiresPolicyDoc(hookName: string): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocking ALLOW-LIST — P3' (2026-04-27)
|
|
3
|
+
*
|
|
4
|
+
* 사용자 작업을 차단(block)할 권한을 가진 hook 의 명시적 화이트리스트.
|
|
5
|
+
* 목록 외 hook 의 부정적 판정은 "관찰 신호"(log only) 로만 처리되어야 한다.
|
|
6
|
+
*
|
|
7
|
+
* RC5 (retro-v040): 분산된 detector 가 각자 block 결정을 내리면서 false-positive
|
|
8
|
+
* 가 메인 로직 흐름까지 차단하는 회귀 패턴 발생. ALLOW-LIST 명시화로 차단 권한
|
|
9
|
+
* 의 source-of-truth 를 단일화.
|
|
10
|
+
*
|
|
11
|
+
* v0.4.2 정책:
|
|
12
|
+
* - 본 모듈은 ALLOW-LIST 정의 + 검증 helper. 기존 deny() 직접 호출 hook 들은
|
|
13
|
+
* v0.4.2 에서 denyOrObserve(name, reason) 로 마이그레이션 완료.
|
|
14
|
+
* - 신규 hook 추가 시 차단 권한이 필요하면 본 ALLOW-LIST 에 추가 + 본 파일의
|
|
15
|
+
* 사유 문서화 의무. 본 commit diff 가 review 필수 항목.
|
|
16
|
+
*
|
|
17
|
+
* 멤버 사유:
|
|
18
|
+
* - stop-guard: Stop hook — false-completion 메타 가드 (자가 검증 강제)
|
|
19
|
+
* - pre-tool-use: Bash dangerous-pattern + 수동 confirm 가드
|
|
20
|
+
* - secret-filter: Write/Edit 결과의 .env / API key 노출 차단
|
|
21
|
+
* - db-guard: Bash 의 destructive DB 명령 (DROP/TRUNCATE/DELETE) 차단
|
|
22
|
+
* - rate-limiter: 사용자 작업 빈도 임계 초과 시 cool-down 차단 (resource abuse 방어)
|
|
23
|
+
*/
|
|
24
|
+
export const BLOCKING_ALLOWLIST = new Set([
|
|
25
|
+
'stop-guard',
|
|
26
|
+
'pre-tool-use',
|
|
27
|
+
'secret-filter',
|
|
28
|
+
'db-guard',
|
|
29
|
+
'rate-limiter',
|
|
30
|
+
]);
|
|
31
|
+
/** hook 이 block 결정을 출력할 권한이 있는지. */
|
|
32
|
+
export function canBlock(hookName) {
|
|
33
|
+
return BLOCKING_ALLOWLIST.has(hookName);
|
|
34
|
+
}
|
|
35
|
+
/** ALLOW-LIST 에 추가하려는 hook 이 정책 문서화를 요구하는지 (lint helper). */
|
|
36
|
+
export function requiresPolicyDoc(hookName) {
|
|
37
|
+
return !BLOCKING_ALLOWLIST.has(hookName);
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-token parser — quote-aware shell command preprocessing.
|
|
3
|
+
*
|
|
4
|
+
* 목적: PreToolUse enforce_via 룰의 정규식이 quote된 인자 텍스트와
|
|
5
|
+
* 명령 토큰을 구분 못 해서 false positive block 발생 (TEST-6, RC5).
|
|
6
|
+
*
|
|
7
|
+
* 사례: forgen compound --solution "title" "본문에 rm -rf 텍스트 포함" 명령이
|
|
8
|
+
* "rm\s+-rf" 패턴에 매칭되어 차단됨. 실제 rm 명령이 아닌데도.
|
|
9
|
+
*
|
|
10
|
+
* 해법: quote된 문자열을 마스킹한 뒤 패턴 매칭. 99% 케이스 커버.
|
|
11
|
+
* 완벽한 shell 파싱은 아니지만 정직하게 한정된 범위.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Mask quoted string contents in a shell command so that text inside
|
|
15
|
+
* single/double quotes, backticks, or $(...) is not matched by patterns
|
|
16
|
+
* intended for command tokens.
|
|
17
|
+
*
|
|
18
|
+
* Examples:
|
|
19
|
+
* maskQuotedContent('rm -rf /') → 'rm -rf /'
|
|
20
|
+
* maskQuotedContent('echo "rm -rf foo"') → 'echo ""'
|
|
21
|
+
* maskQuotedContent("forgen save 'rm -rf body'") → "forgen save ''"
|
|
22
|
+
* maskQuotedContent('rm -rf $(pwd)') → 'rm -rf $()'
|
|
23
|
+
* maskQuotedContent('echo `rm -rf x`') → 'echo ``'
|
|
24
|
+
*
|
|
25
|
+
* Limitations (documented, not silently broken):
|
|
26
|
+
* - escaped quotes inside quoted strings: best-effort only
|
|
27
|
+
* - heredoc bodies (<<EOF ... EOF): masked as `<<HEREDOC>>` (v0.4.1+)
|
|
28
|
+
* - nested $(...) / `...`: outer level masked
|
|
29
|
+
*/
|
|
30
|
+
export declare function maskQuotedContent(cmd: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Decide if a verifier should match against the raw command, masked command,
|
|
33
|
+
* or the leading command tokens of each statement.
|
|
34
|
+
*
|
|
35
|
+
* 'raw' — backward compat. Match against the unmodified command string.
|
|
36
|
+
* 'masked' — Strip quoted contents first. Use this when the rule wants to
|
|
37
|
+
* guard a real command invocation (e.g. rm -rf) and not text
|
|
38
|
+
* inside string literals passed as arguments to other commands.
|
|
39
|
+
* 'command_tokens' — Reserved for future use (per-statement leading-token check).
|
|
40
|
+
* Currently behaves like 'masked' to avoid silently breaking
|
|
41
|
+
* when rule files use it.
|
|
42
|
+
*/
|
|
43
|
+
export type MatchTarget = 'raw' | 'masked' | 'command_tokens';
|
|
44
|
+
export declare function preprocessForMatch(cmd: string, target: MatchTarget | undefined): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-token parser — quote-aware shell command preprocessing.
|
|
3
|
+
*
|
|
4
|
+
* 목적: PreToolUse enforce_via 룰의 정규식이 quote된 인자 텍스트와
|
|
5
|
+
* 명령 토큰을 구분 못 해서 false positive block 발생 (TEST-6, RC5).
|
|
6
|
+
*
|
|
7
|
+
* 사례: forgen compound --solution "title" "본문에 rm -rf 텍스트 포함" 명령이
|
|
8
|
+
* "rm\s+-rf" 패턴에 매칭되어 차단됨. 실제 rm 명령이 아닌데도.
|
|
9
|
+
*
|
|
10
|
+
* 해법: quote된 문자열을 마스킹한 뒤 패턴 매칭. 99% 케이스 커버.
|
|
11
|
+
* 완벽한 shell 파싱은 아니지만 정직하게 한정된 범위.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Mask quoted string contents in a shell command so that text inside
|
|
15
|
+
* single/double quotes, backticks, or $(...) is not matched by patterns
|
|
16
|
+
* intended for command tokens.
|
|
17
|
+
*
|
|
18
|
+
* Examples:
|
|
19
|
+
* maskQuotedContent('rm -rf /') → 'rm -rf /'
|
|
20
|
+
* maskQuotedContent('echo "rm -rf foo"') → 'echo ""'
|
|
21
|
+
* maskQuotedContent("forgen save 'rm -rf body'") → "forgen save ''"
|
|
22
|
+
* maskQuotedContent('rm -rf $(pwd)') → 'rm -rf $()'
|
|
23
|
+
* maskQuotedContent('echo `rm -rf x`') → 'echo ``'
|
|
24
|
+
*
|
|
25
|
+
* Limitations (documented, not silently broken):
|
|
26
|
+
* - escaped quotes inside quoted strings: best-effort only
|
|
27
|
+
* - heredoc bodies (<<EOF ... EOF): masked as `<<HEREDOC>>` (v0.4.1+)
|
|
28
|
+
* - nested $(...) / `...`: outer level masked
|
|
29
|
+
*/
|
|
30
|
+
export function maskQuotedContent(cmd) {
|
|
31
|
+
if (!cmd)
|
|
32
|
+
return cmd;
|
|
33
|
+
let out = cmd;
|
|
34
|
+
// v0.4.1 (2026-04-24) — heredoc body 마스킹 추가. 이전엔 `cat > f <<EOF\n rm -rf /tmp \nEOF`
|
|
35
|
+
// 처럼 heredoc 본문이 command string 에 포함돼 false-positive block 발생.
|
|
36
|
+
// 지원 형식: <<EOF / <<'EOF' / <<"EOF" / <<-EOF (indent 무시 변종).
|
|
37
|
+
// <<-MARK 은 indent 허용 (terminator 앞 whitespace). `\n\s*\2` 로 반영.
|
|
38
|
+
out = out.replace(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?\n\s*\2\b/g, '<<HEREDOC>>');
|
|
39
|
+
// Order matters: command substitution before plain quotes (they may contain quotes themselves).
|
|
40
|
+
out = out.replace(/\$\([^)]*\)/g, '$()');
|
|
41
|
+
out = out.replace(/`[^`]*`/g, '``');
|
|
42
|
+
out = out.replace(/'[^']*'/g, "''");
|
|
43
|
+
out = out.replace(/"[^"]*"/g, '""');
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export function preprocessForMatch(cmd, target) {
|
|
47
|
+
if (!target || target === 'raw')
|
|
48
|
+
return cmd;
|
|
49
|
+
return maskQuotedContent(cmd);
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge Loop State — RC6 가드 (US-M1)
|
|
3
|
+
*
|
|
4
|
+
* 직전 forge-loop 의 findings 또는 진행 중 stories 를 ≤1KB 요약으로 렌더한다.
|
|
5
|
+
* SessionStart 와 UserPromptSubmit 두 hook 이 공유하는 단일 진입점.
|
|
6
|
+
*
|
|
7
|
+
* RC6 자기증거: 본 세션 R1 에서 head -80 으로 forge-loop.json 을 읽어 findings
|
|
8
|
+
* 8줄(line 92~99)이 잘렸음. 결과적으로 직전 결론을 컨텍스트에 못 가져 같은
|
|
9
|
+
* 가설을 재발. 이 모듈은 그 회귀를 시스템 레벨에서 차단한다.
|
|
10
|
+
*/
|
|
11
|
+
interface ForgeLoopStory {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
passes?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ForgeLoopState {
|
|
17
|
+
active?: boolean;
|
|
18
|
+
task?: string;
|
|
19
|
+
startedAt?: string;
|
|
20
|
+
completedAt?: string;
|
|
21
|
+
stories?: ForgeLoopStory[];
|
|
22
|
+
findings?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export declare function readForgeLoopState(filePath?: string): ForgeLoopState | null;
|
|
25
|
+
/** SessionStart 용 — 완료된 forge-loop 의 findings 또는 진행 중 stories 요약. */
|
|
26
|
+
export declare function renderForgeLoopForSession(state: ForgeLoopState | null, now?: number): string | null;
|
|
27
|
+
/** UserPromptSubmit 용 — active=true 시에만 짧은 진행 상황 1~2줄. */
|
|
28
|
+
export declare function renderForgeLoopForPrompt(state: ForgeLoopState | null, now?: number): string | null;
|
|
29
|
+
/** 테스트 노출용 상수 — 회귀 시 임계값 변경 즉시 감지. */
|
|
30
|
+
export declare const FORGE_LOOP_LIMITS: {
|
|
31
|
+
readonly SOFT_STALE_MS: number;
|
|
32
|
+
readonly HARD_STALE_MS: number;
|
|
33
|
+
readonly MAX_INJECT_BYTES: 1024;
|
|
34
|
+
readonly MAX_PENDING: 5;
|
|
35
|
+
};
|
|
36
|
+
export {};
|