@wooojin/forgen 0.1.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 +20 -0
- package/CHANGELOG.md +353 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +21 -0
- package/README.ja.md +469 -0
- package/README.ko.md +469 -0
- package/README.md +483 -0
- package/README.zh.md +469 -0
- package/agents/analyst.md +98 -0
- package/agents/architect.md +62 -0
- package/agents/code-reviewer.md +120 -0
- package/agents/code-simplifier.md +197 -0
- package/agents/critic.md +70 -0
- package/agents/debugger.md +117 -0
- package/agents/designer.md +131 -0
- package/agents/executor.md +54 -0
- package/agents/explore.md +145 -0
- package/agents/git-master.md +212 -0
- package/agents/performance-reviewer.md +172 -0
- package/agents/planner.md +29 -0
- package/agents/qa-tester.md +158 -0
- package/agents/refactoring-expert.md +168 -0
- package/agents/scientist.md +144 -0
- package/agents/security-reviewer.md +137 -0
- package/agents/test-engineer.md +153 -0
- package/agents/verifier.md +133 -0
- package/agents/writer.md +184 -0
- package/commands/api-design.md +268 -0
- package/commands/architecture-decision.md +314 -0
- package/commands/ci-cd.md +270 -0
- package/commands/code-review.md +233 -0
- package/commands/compound.md +117 -0
- package/commands/database.md +263 -0
- package/commands/debug-detective.md +99 -0
- package/commands/docker.md +274 -0
- package/commands/documentation.md +276 -0
- package/commands/ecomode.md +51 -0
- package/commands/frontend.md +271 -0
- package/commands/git-master.md +90 -0
- package/commands/incident-response.md +292 -0
- package/commands/migrate.md +101 -0
- package/commands/performance.md +288 -0
- package/commands/refactor.md +105 -0
- package/commands/security-review.md +288 -0
- package/commands/tdd.md +183 -0
- package/commands/testing-strategy.md +265 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/core/auto-compound-runner.d.ts +12 -0
- package/dist/core/auto-compound-runner.js +460 -0
- package/dist/core/config-hooks.d.ts +10 -0
- package/dist/core/config-hooks.js +112 -0
- package/dist/core/config-injector.d.ts +50 -0
- package/dist/core/config-injector.js +455 -0
- package/dist/core/doctor.d.ts +1 -0
- package/dist/core/doctor.js +163 -0
- package/dist/core/errors.d.ts +81 -0
- package/dist/core/errors.js +133 -0
- package/dist/core/global-config.d.ts +43 -0
- package/dist/core/global-config.js +25 -0
- package/dist/core/harness.d.ts +24 -0
- package/dist/core/harness.js +621 -0
- package/dist/core/init.d.ts +7 -0
- package/dist/core/init.js +37 -0
- package/dist/core/inspect-cli.d.ts +7 -0
- package/dist/core/inspect-cli.js +47 -0
- package/dist/core/legacy-detector.d.ts +33 -0
- package/dist/core/legacy-detector.js +66 -0
- package/dist/core/logger.d.ts +34 -0
- package/dist/core/logger.js +121 -0
- package/dist/core/mcp-config.d.ts +44 -0
- package/dist/core/mcp-config.js +177 -0
- package/dist/core/notepad.d.ts +31 -0
- package/dist/core/notepad.js +88 -0
- package/dist/core/paths.d.ts +85 -0
- package/dist/core/paths.js +101 -0
- package/dist/core/plugin-detector.d.ts +44 -0
- package/dist/core/plugin-detector.js +226 -0
- package/dist/core/runtime-detector.d.ts +8 -0
- package/dist/core/runtime-detector.js +49 -0
- package/dist/core/scope-resolver.d.ts +8 -0
- package/dist/core/scope-resolver.js +45 -0
- package/dist/core/session-logger.d.ts +6 -0
- package/dist/core/session-logger.js +111 -0
- package/dist/core/session-store.d.ts +28 -0
- package/dist/core/session-store.js +218 -0
- package/dist/core/settings-lock.d.ts +18 -0
- package/dist/core/settings-lock.js +125 -0
- package/dist/core/spawn.d.ts +3 -0
- package/dist/core/spawn.js +135 -0
- package/dist/core/types.d.ts +108 -0
- package/dist/core/types.js +1 -0
- package/dist/core/uninstall.d.ts +4 -0
- package/dist/core/uninstall.js +307 -0
- package/dist/core/v1-bootstrap.d.ts +26 -0
- package/dist/core/v1-bootstrap.js +155 -0
- package/dist/engine/compound-cli.d.ts +24 -0
- package/dist/engine/compound-cli.js +250 -0
- package/dist/engine/compound-extractor.d.ts +68 -0
- package/dist/engine/compound-extractor.js +860 -0
- package/dist/engine/compound-lifecycle.d.ts +32 -0
- package/dist/engine/compound-lifecycle.js +305 -0
- package/dist/engine/compound-loop.d.ts +32 -0
- package/dist/engine/compound-loop.js +511 -0
- package/dist/engine/match-eval-log.d.ts +139 -0
- package/dist/engine/match-eval-log.js +270 -0
- package/dist/engine/phrase-blocklist.d.ts +119 -0
- package/dist/engine/phrase-blocklist.js +208 -0
- package/dist/engine/skill-promoter.d.ts +20 -0
- package/dist/engine/skill-promoter.js +115 -0
- package/dist/engine/solution-format.d.ts +160 -0
- package/dist/engine/solution-format.js +432 -0
- package/dist/engine/solution-index.d.ts +13 -0
- package/dist/engine/solution-index.js +252 -0
- package/dist/engine/solution-matcher.d.ts +364 -0
- package/dist/engine/solution-matcher.js +656 -0
- package/dist/engine/solution-writer.d.ts +76 -0
- package/dist/engine/solution-writer.js +157 -0
- package/dist/engine/term-matcher.d.ts +81 -0
- package/dist/engine/term-matcher.js +268 -0
- package/dist/engine/term-normalizer.d.ts +116 -0
- package/dist/engine/term-normalizer.js +171 -0
- package/dist/fgx.d.ts +6 -0
- package/dist/fgx.js +42 -0
- package/dist/forge/cli.d.ts +11 -0
- package/dist/forge/cli.js +100 -0
- package/dist/forge/evidence-processor.d.ts +21 -0
- package/dist/forge/evidence-processor.js +87 -0
- package/dist/forge/mismatch-detector.d.ts +44 -0
- package/dist/forge/mismatch-detector.js +83 -0
- package/dist/forge/onboarding-cli.d.ts +6 -0
- package/dist/forge/onboarding-cli.js +89 -0
- package/dist/forge/onboarding.d.ts +25 -0
- package/dist/forge/onboarding.js +122 -0
- package/dist/hooks/compound-reflection.d.ts +45 -0
- package/dist/hooks/compound-reflection.js +82 -0
- package/dist/hooks/context-guard.d.ts +24 -0
- package/dist/hooks/context-guard.js +156 -0
- package/dist/hooks/dangerous-patterns.json +18 -0
- package/dist/hooks/db-guard.d.ts +17 -0
- package/dist/hooks/db-guard.js +105 -0
- package/dist/hooks/hook-config.d.ts +29 -0
- package/dist/hooks/hook-config.js +92 -0
- package/dist/hooks/hook-registry.d.ts +43 -0
- package/dist/hooks/hook-registry.js +31 -0
- package/dist/hooks/hooks-generator.d.ts +49 -0
- package/dist/hooks/hooks-generator.js +99 -0
- package/dist/hooks/intent-classifier.d.ts +12 -0
- package/dist/hooks/intent-classifier.js +62 -0
- package/dist/hooks/keyword-detector.d.ts +25 -0
- package/dist/hooks/keyword-detector.js +389 -0
- package/dist/hooks/notepad-injector.d.ts +18 -0
- package/dist/hooks/notepad-injector.js +51 -0
- package/dist/hooks/permission-handler.d.ts +14 -0
- package/dist/hooks/permission-handler.js +114 -0
- package/dist/hooks/post-tool-failure.d.ts +11 -0
- package/dist/hooks/post-tool-failure.js +118 -0
- package/dist/hooks/post-tool-handlers.d.ts +17 -0
- package/dist/hooks/post-tool-handlers.js +115 -0
- package/dist/hooks/post-tool-use.d.ts +29 -0
- package/dist/hooks/post-tool-use.js +151 -0
- package/dist/hooks/pre-compact.d.ts +10 -0
- package/dist/hooks/pre-compact.js +165 -0
- package/dist/hooks/pre-tool-use.d.ts +31 -0
- package/dist/hooks/pre-tool-use.js +325 -0
- package/dist/hooks/prompt-injection-filter.d.ts +56 -0
- package/dist/hooks/prompt-injection-filter.js +287 -0
- package/dist/hooks/rate-limiter.d.ts +21 -0
- package/dist/hooks/rate-limiter.js +86 -0
- package/dist/hooks/secret-filter.d.ts +14 -0
- package/dist/hooks/secret-filter.js +65 -0
- package/dist/hooks/session-recovery.d.ts +27 -0
- package/dist/hooks/session-recovery.js +406 -0
- package/dist/hooks/shared/atomic-write.d.ts +41 -0
- package/dist/hooks/shared/atomic-write.js +148 -0
- package/dist/hooks/shared/context-budget.d.ts +37 -0
- package/dist/hooks/shared/context-budget.js +45 -0
- package/dist/hooks/shared/file-lock.d.ts +56 -0
- package/dist/hooks/shared/file-lock.js +253 -0
- package/dist/hooks/shared/hook-response.d.ts +33 -0
- package/dist/hooks/shared/hook-response.js +62 -0
- package/dist/hooks/shared/injection-caps.d.ts +39 -0
- package/dist/hooks/shared/injection-caps.js +52 -0
- package/dist/hooks/shared/plugin-signal.d.ts +23 -0
- package/dist/hooks/shared/plugin-signal.js +104 -0
- package/dist/hooks/shared/read-stdin.d.ts +8 -0
- package/dist/hooks/shared/read-stdin.js +63 -0
- package/dist/hooks/shared/sanitize-id.d.ts +7 -0
- package/dist/hooks/shared/sanitize-id.js +9 -0
- package/dist/hooks/shared/sanitize.d.ts +7 -0
- package/dist/hooks/shared/sanitize.js +22 -0
- package/dist/hooks/skill-injector.d.ts +38 -0
- package/dist/hooks/skill-injector.js +285 -0
- package/dist/hooks/slop-detector.d.ts +18 -0
- package/dist/hooks/slop-detector.js +93 -0
- package/dist/hooks/solution-injector.d.ts +58 -0
- package/dist/hooks/solution-injector.js +436 -0
- package/dist/hooks/subagent-tracker.d.ts +10 -0
- package/dist/hooks/subagent-tracker.js +90 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +224 -0
- package/dist/lib.d.ts +14 -0
- package/dist/lib.js +14 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +40 -0
- package/dist/mcp/solution-reader.d.ts +90 -0
- package/dist/mcp/solution-reader.js +273 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +302 -0
- package/dist/preset/facet-catalog.d.ts +17 -0
- package/dist/preset/facet-catalog.js +46 -0
- package/dist/preset/preset-manager.d.ts +31 -0
- package/dist/preset/preset-manager.js +111 -0
- package/dist/renderer/inspect-renderer.d.ts +11 -0
- package/dist/renderer/inspect-renderer.js +123 -0
- package/dist/renderer/rule-renderer.d.ts +18 -0
- package/dist/renderer/rule-renderer.js +159 -0
- package/dist/store/evidence-store.d.ts +23 -0
- package/dist/store/evidence-store.js +58 -0
- package/dist/store/profile-store.d.ts +12 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/recommendation-store.d.ts +22 -0
- package/dist/store/recommendation-store.js +64 -0
- package/dist/store/rule-store.d.ts +22 -0
- package/dist/store/rule-store.js +62 -0
- package/dist/store/session-state-store.d.ts +11 -0
- package/dist/store/session-state-store.js +44 -0
- package/dist/store/types.d.ts +159 -0
- package/dist/store/types.js +7 -0
- package/hooks/hook-registry.json +21 -0
- package/hooks/hooks.json +185 -0
- package/package.json +89 -0
- package/plugin.json +20 -0
- package/scripts/postinstall.js +826 -0
- package/skills/api-design/SKILL.md +262 -0
- package/skills/architecture-decision/SKILL.md +309 -0
- package/skills/ci-cd/SKILL.md +264 -0
- package/skills/code-review/SKILL.md +228 -0
- package/skills/compound/SKILL.md +101 -0
- package/skills/database/SKILL.md +257 -0
- package/skills/debug-detective/SKILL.md +95 -0
- package/skills/docker/SKILL.md +268 -0
- package/skills/documentation/SKILL.md +270 -0
- package/skills/ecomode/SKILL.md +46 -0
- package/skills/frontend/SKILL.md +265 -0
- package/skills/git-master/SKILL.md +86 -0
- package/skills/incident-response/SKILL.md +286 -0
- package/skills/migrate/SKILL.md +96 -0
- package/skills/performance/SKILL.md +282 -0
- package/skills/refactor/SKILL.md +100 -0
- package/skills/security-review/SKILL.md +282 -0
- package/skills/tdd/SKILL.md +178 -0
- package/skills/testing-strategy/SKILL.md +260 -0
- package/starter-pack/solutions/starter-api-error-responses.md +37 -0
- package/starter-pack/solutions/starter-async-patterns.md +40 -0
- package/starter-pack/solutions/starter-caching-strategy.md +40 -0
- package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
- package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
- package/starter-pack/solutions/starter-dependency-injection.md +40 -0
- package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
- package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
- package/starter-pack/solutions/starter-input-validation.md +40 -0
- package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
- package/starter-pack/solutions/starter-refactor-safely.md +38 -0
- package/starter-pack/solutions/starter-secret-management.md +37 -0
- package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
- package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
- package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Pack Mismatch Detector
|
|
3
|
+
*
|
|
4
|
+
* rolling 3세션 mismatch score 계산.
|
|
5
|
+
* Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §8
|
|
6
|
+
*
|
|
7
|
+
* 신호별 점수:
|
|
8
|
+
* - 반대 방향 explicit_correction: +2
|
|
9
|
+
* - session_summary가 반대 pack 성향 명시: +1
|
|
10
|
+
* - 같은 축 strong rule 2개+ 신규 생성: +1
|
|
11
|
+
*
|
|
12
|
+
* 후보 조건:
|
|
13
|
+
* - 최근 3세션 rolling sum >= 4 (축별)
|
|
14
|
+
* - 또는 최근 3세션에 같은 방향 explicit_correction 2회+
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 단일 세션의 mismatch 신호를 계산.
|
|
18
|
+
*
|
|
19
|
+
* @param sessionId 세션 ID
|
|
20
|
+
* @param corrections 해당 세션의 explicit_correction evidence
|
|
21
|
+
* @param summaries 해당 세션의 session_summary evidence
|
|
22
|
+
* @param newStrongRules 해당 세션에서 신규 생성된 strong rule
|
|
23
|
+
* @param currentQuality 현재 quality pack
|
|
24
|
+
* @param currentAutonomy 현재 autonomy pack
|
|
25
|
+
*/
|
|
26
|
+
export function computeSessionSignals(sessionId, corrections, summaries, newStrongRules, _currentQuality, _currentAutonomy) {
|
|
27
|
+
const signals = [];
|
|
28
|
+
// 반대 방향 explicit_correction
|
|
29
|
+
for (const c of corrections) {
|
|
30
|
+
for (const axis of c.axis_refs) {
|
|
31
|
+
if (axis === 'quality_safety' || axis === 'autonomy') {
|
|
32
|
+
// correction의 raw_payload에 direction 힌트가 있으면 pack과 비교
|
|
33
|
+
const direction = c.raw_payload?.direction;
|
|
34
|
+
if (direction === 'opposite') {
|
|
35
|
+
signals.push({ session_id: sessionId, axis, score: 2, reason: `반대 방향 correction: ${c.summary}` });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// session_summary가 반대 성향 명시
|
|
41
|
+
for (const s of summaries) {
|
|
42
|
+
const packHint = s.raw_payload?.pack_direction;
|
|
43
|
+
if (packHint === 'opposite_quality') {
|
|
44
|
+
signals.push({ session_id: sessionId, axis: 'quality_safety', score: 1, reason: `session summary 반대 성향: ${s.summary}` });
|
|
45
|
+
}
|
|
46
|
+
if (packHint === 'opposite_autonomy') {
|
|
47
|
+
signals.push({ session_id: sessionId, axis: 'autonomy', score: 1, reason: `session summary 반대 성향: ${s.summary}` });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 같은 축 strong rule 2개+ 신규 생성
|
|
51
|
+
const qualityStrong = newStrongRules.filter(r => r.category === 'quality' && r.strength === 'strong');
|
|
52
|
+
const autonomyStrong = newStrongRules.filter(r => r.category === 'autonomy' && r.strength === 'strong');
|
|
53
|
+
if (qualityStrong.length >= 2) {
|
|
54
|
+
signals.push({ session_id: sessionId, axis: 'quality_safety', score: 1, reason: `${qualityStrong.length}개 strong quality rule 신규 생성` });
|
|
55
|
+
}
|
|
56
|
+
if (autonomyStrong.length >= 2) {
|
|
57
|
+
signals.push({ session_id: sessionId, axis: 'autonomy', score: 1, reason: `${autonomyStrong.length}개 strong autonomy rule 신규 생성` });
|
|
58
|
+
}
|
|
59
|
+
return signals;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 최근 3세션 rolling sum으로 mismatch 판정.
|
|
63
|
+
*/
|
|
64
|
+
export function detectMismatch(recentSignals) {
|
|
65
|
+
let qualityScore = 0;
|
|
66
|
+
let autonomyScore = 0;
|
|
67
|
+
for (const s of recentSignals) {
|
|
68
|
+
if (s.axis === 'quality_safety')
|
|
69
|
+
qualityScore += s.score;
|
|
70
|
+
if (s.axis === 'autonomy')
|
|
71
|
+
autonomyScore += s.score;
|
|
72
|
+
}
|
|
73
|
+
// 같은 방향 correction 2회+ 체크
|
|
74
|
+
const qualityCorrections = recentSignals.filter(s => s.axis === 'quality_safety' && s.score === 2);
|
|
75
|
+
const autonomyCorrections = recentSignals.filter(s => s.axis === 'autonomy' && s.score === 2);
|
|
76
|
+
return {
|
|
77
|
+
quality_mismatch: qualityScore >= 4 || qualityCorrections.length >= 2,
|
|
78
|
+
autonomy_mismatch: autonomyScore >= 4 || autonomyCorrections.length >= 2,
|
|
79
|
+
quality_score: qualityScore,
|
|
80
|
+
autonomy_score: autonomyScore,
|
|
81
|
+
signals: recentSignals,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Onboarding CLI
|
|
3
|
+
*
|
|
4
|
+
* 언어 선택 + 4문항 온보딩 인터랙티브 flow.
|
|
5
|
+
*/
|
|
6
|
+
import * as readline from 'node:readline';
|
|
7
|
+
import { computeOnboarding, onboardingToRecommendation } from './onboarding.js';
|
|
8
|
+
import { createProfile, saveProfile } from '../store/profile-store.js';
|
|
9
|
+
import { saveRecommendation, updateRecommendationStatus } from '../store/recommendation-store.js';
|
|
10
|
+
import { ensureV1Directories } from '../core/v1-bootstrap.js';
|
|
11
|
+
import { ONBOARDING, qualityName, autonomyName, judgmentName, communicationName, trustName, setLocale } from '../i18n/index.js';
|
|
12
|
+
import { saveGlobalConfig, loadGlobalConfig } from '../core/global-config.js';
|
|
13
|
+
function askChoice(rl, question, validChoices, errorMsg) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const ask = () => {
|
|
16
|
+
rl.question(question, (answer) => {
|
|
17
|
+
const upper = answer.trim().toUpperCase();
|
|
18
|
+
if (validChoices.includes(upper)) {
|
|
19
|
+
resolve(upper);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(errorMsg);
|
|
23
|
+
ask();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
ask();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function runOnboarding() {
|
|
31
|
+
ensureV1Directories();
|
|
32
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
+
try {
|
|
34
|
+
// 0. 언어 선택
|
|
35
|
+
console.log(`
|
|
36
|
+
╔══════════════════════════════════════════════╗
|
|
37
|
+
║ Forgen — Setup ║
|
|
38
|
+
╚══════════════════════════════════════════════╝
|
|
39
|
+
|
|
40
|
+
Select language / 언어를 선택하세요:
|
|
41
|
+
|
|
42
|
+
1) English
|
|
43
|
+
2) 한국어
|
|
44
|
+
`);
|
|
45
|
+
const langChoice = await askChoice(rl, ' Choice / 선택 (1/2): ', ['1', '2'], ' Please enter 1 or 2. / 1 또는 2를 입력하세요.');
|
|
46
|
+
const locale = langChoice === '2' ? 'ko' : 'en';
|
|
47
|
+
setLocale(locale);
|
|
48
|
+
// locale을 GlobalConfig에 저장
|
|
49
|
+
const config = loadGlobalConfig();
|
|
50
|
+
config.locale = locale;
|
|
51
|
+
saveGlobalConfig(config);
|
|
52
|
+
const strings = ONBOARDING[locale];
|
|
53
|
+
console.log(`
|
|
54
|
+
╔══════════════════════════════════════════════╗
|
|
55
|
+
║${strings.header.padEnd(46)}║
|
|
56
|
+
╚══════════════════════════════════════════════╝
|
|
57
|
+
|
|
58
|
+
${strings.subtitle}`);
|
|
59
|
+
// 1-4. 4문항
|
|
60
|
+
const q1 = await askChoice(rl, strings.q1, ['A', 'B', 'C'], strings.invalidChoice);
|
|
61
|
+
const q2 = await askChoice(rl, strings.q2, ['A', 'B', 'C'], strings.invalidChoice);
|
|
62
|
+
const q3 = await askChoice(rl, strings.q3, ['A', 'B', 'C'], strings.invalidChoice);
|
|
63
|
+
const q4 = await askChoice(rl, strings.q4, ['A', 'B', 'C'], strings.invalidChoice);
|
|
64
|
+
const result = computeOnboarding(q1, q2, q3, q4);
|
|
65
|
+
console.log(`
|
|
66
|
+
─────────────────────────────────────────
|
|
67
|
+
${strings.resultHeader}
|
|
68
|
+
|
|
69
|
+
Quality: ${qualityName(result.qualityPack, locale)} (confidence: ${result.qualityConfidence.toFixed(2)})
|
|
70
|
+
Autonomy: ${autonomyName(result.autonomyPack, locale)} (confidence: ${result.autonomyConfidence.toFixed(2)})
|
|
71
|
+
Judgment: ${judgmentName(result.judgmentPack, locale)} (confidence: ${result.judgmentConfidence.toFixed(2)})
|
|
72
|
+
Communication: ${communicationName(result.communicationPack, locale)} (confidence: ${result.communicationConfidence.toFixed(2)})
|
|
73
|
+
Trust: ${trustName(result.suggestedTrustPolicy, locale)}
|
|
74
|
+
─────────────────────────────────────────`);
|
|
75
|
+
// Recommendation 저장
|
|
76
|
+
const rec = onboardingToRecommendation(result);
|
|
77
|
+
saveRecommendation(rec);
|
|
78
|
+
updateRecommendationStatus(rec.recommendation_id, 'accepted');
|
|
79
|
+
// Profile 생성
|
|
80
|
+
const profile = createProfile('default', result.qualityPack, result.autonomyPack, result.suggestedTrustPolicy, 'onboarding', result.judgmentPack, result.communicationPack);
|
|
81
|
+
saveProfile(profile);
|
|
82
|
+
console.log(`
|
|
83
|
+
${strings.profileSaved}
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
rl.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Onboarding
|
|
3
|
+
*
|
|
4
|
+
* 2문항 온보딩, 점수 계산, pack 추천.
|
|
5
|
+
* Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §3-4
|
|
6
|
+
*/
|
|
7
|
+
import type { QualityPack, AutonomyPack, JudgmentPack, CommunicationPack, TrustPolicy, PackRecommendation } from '../store/types.js';
|
|
8
|
+
export type ChoiceId = 'A' | 'B' | 'C';
|
|
9
|
+
export interface OnboardingResult {
|
|
10
|
+
qualityScore: number;
|
|
11
|
+
autonomyScore: number;
|
|
12
|
+
judgmentScore: number;
|
|
13
|
+
communicationScore: number;
|
|
14
|
+
qualityPack: QualityPack;
|
|
15
|
+
autonomyPack: AutonomyPack;
|
|
16
|
+
judgmentPack: JudgmentPack;
|
|
17
|
+
communicationPack: CommunicationPack;
|
|
18
|
+
qualityConfidence: number;
|
|
19
|
+
autonomyConfidence: number;
|
|
20
|
+
judgmentConfidence: number;
|
|
21
|
+
communicationConfidence: number;
|
|
22
|
+
suggestedTrustPolicy: TrustPolicy;
|
|
23
|
+
}
|
|
24
|
+
export declare function computeOnboarding(q1: ChoiceId, q2: ChoiceId, q3?: ChoiceId, q4?: ChoiceId): OnboardingResult;
|
|
25
|
+
export declare function onboardingToRecommendation(result: OnboardingResult): PackRecommendation;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Onboarding
|
|
3
|
+
*
|
|
4
|
+
* 2문항 온보딩, 점수 계산, pack 추천.
|
|
5
|
+
* Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §3-4
|
|
6
|
+
*/
|
|
7
|
+
import { createRecommendation } from '../store/recommendation-store.js';
|
|
8
|
+
// 질문 1: 애매한 구현 요청 + 인접 영향 가능성
|
|
9
|
+
// 주 판별: 자율 실행 성향, 보조: 품질/안전
|
|
10
|
+
const Q1_SCORES = {
|
|
11
|
+
A: { quality: -1, autonomy: -2 },
|
|
12
|
+
B: { quality: 0, autonomy: 0 },
|
|
13
|
+
C: { quality: +1, autonomy: +2 },
|
|
14
|
+
};
|
|
15
|
+
// 질문 2: 수정 완료 직전 + 검증 강도 vs 완료 속도
|
|
16
|
+
// 주 판별: 품질/안전, 보조: 자율 실행 성향
|
|
17
|
+
const Q2_SCORES = {
|
|
18
|
+
A: { quality: -2, autonomy: 0 },
|
|
19
|
+
B: { quality: 0, autonomy: 0 },
|
|
20
|
+
C: { quality: +2, autonomy: +1 },
|
|
21
|
+
};
|
|
22
|
+
// 질문 3: 코드 수정 접근법 — 최소변경 vs 구조적 리팩토링
|
|
23
|
+
// 주 판별: 판단 철학
|
|
24
|
+
const Q3_SCORES = {
|
|
25
|
+
A: { judgment: -2 }, // 최소 변경 우선
|
|
26
|
+
B: { judgment: 0 },
|
|
27
|
+
C: { judgment: +2 }, // 구조적 정리 우선
|
|
28
|
+
};
|
|
29
|
+
// 질문 4: 설명/보고 스타일 — 간결 vs 상세
|
|
30
|
+
// 주 판별: 커뮤니케이션 스타일
|
|
31
|
+
const Q4_SCORES = {
|
|
32
|
+
A: { communication: +2 }, // 상세 설명 선호
|
|
33
|
+
B: { communication: 0 },
|
|
34
|
+
C: { communication: -2 }, // 간결 선호
|
|
35
|
+
};
|
|
36
|
+
// ── Pack 매핑 ──
|
|
37
|
+
function qualityFromScore(score) {
|
|
38
|
+
if (score <= -2)
|
|
39
|
+
return '보수형';
|
|
40
|
+
if (score >= 2)
|
|
41
|
+
return '속도형';
|
|
42
|
+
return '균형형';
|
|
43
|
+
}
|
|
44
|
+
function autonomyFromScore(score) {
|
|
45
|
+
if (score <= -2)
|
|
46
|
+
return '확인 우선형';
|
|
47
|
+
if (score >= 2)
|
|
48
|
+
return '자율 실행형';
|
|
49
|
+
return '균형형';
|
|
50
|
+
}
|
|
51
|
+
function judgmentFromScore(score) {
|
|
52
|
+
if (score <= -1)
|
|
53
|
+
return '최소변경형';
|
|
54
|
+
if (score >= 1)
|
|
55
|
+
return '구조적접근형';
|
|
56
|
+
return '균형형';
|
|
57
|
+
}
|
|
58
|
+
function communicationFromScore(score) {
|
|
59
|
+
if (score <= -1)
|
|
60
|
+
return '간결형';
|
|
61
|
+
if (score >= 1)
|
|
62
|
+
return '상세형';
|
|
63
|
+
return '균형형';
|
|
64
|
+
}
|
|
65
|
+
// ── Confidence ──
|
|
66
|
+
function computeConfidence(score, q1Contribution, q2Contribution) {
|
|
67
|
+
// contradiction: 2문항 기여 부호가 반대일 때
|
|
68
|
+
const contradictions = (q1Contribution > 0 && q2Contribution < 0) || (q1Contribution < 0 && q2Contribution > 0) ? 1 : 0;
|
|
69
|
+
const raw = 0.45 + (0.2 * Math.abs(score)) - (0.15 * contradictions);
|
|
70
|
+
return Math.max(0.2, Math.min(0.95, raw));
|
|
71
|
+
}
|
|
72
|
+
// ── Trust Policy 추천 ──
|
|
73
|
+
const TRUST_MAP = {
|
|
74
|
+
'보수형+확인 우선형': '가드레일 우선',
|
|
75
|
+
'속도형+자율 실행형': '완전 신뢰 실행',
|
|
76
|
+
};
|
|
77
|
+
function suggestTrustPolicy(quality, autonomy) {
|
|
78
|
+
return TRUST_MAP[`${quality}+${autonomy}`] ?? '승인 완화';
|
|
79
|
+
}
|
|
80
|
+
export function computeOnboarding(q1, q2, q3 = 'B', q4 = 'B') {
|
|
81
|
+
const qualityScore = Q1_SCORES[q1].quality + Q2_SCORES[q2].quality;
|
|
82
|
+
const autonomyScore = Q1_SCORES[q1].autonomy + Q2_SCORES[q2].autonomy;
|
|
83
|
+
const judgmentScore = Q3_SCORES[q3].judgment;
|
|
84
|
+
const communicationScore = Q4_SCORES[q4].communication;
|
|
85
|
+
const qualityPack = qualityFromScore(qualityScore);
|
|
86
|
+
const autonomyPack = autonomyFromScore(autonomyScore);
|
|
87
|
+
const judgmentPack = judgmentFromScore(judgmentScore);
|
|
88
|
+
const communicationPack = communicationFromScore(communicationScore);
|
|
89
|
+
const qualityConfidence = computeConfidence(qualityScore, Q1_SCORES[q1].quality, Q2_SCORES[q2].quality);
|
|
90
|
+
const autonomyConfidence = computeConfidence(autonomyScore, Q1_SCORES[q1].autonomy, Q2_SCORES[q2].autonomy);
|
|
91
|
+
// Q3, Q4는 단일 질문이므로 confidence가 낮음
|
|
92
|
+
const judgmentConfidence = Math.max(0.2, Math.min(0.75, 0.35 + 0.2 * Math.abs(judgmentScore)));
|
|
93
|
+
const communicationConfidence = Math.max(0.2, Math.min(0.75, 0.35 + 0.2 * Math.abs(communicationScore)));
|
|
94
|
+
return {
|
|
95
|
+
qualityScore,
|
|
96
|
+
autonomyScore,
|
|
97
|
+
judgmentScore,
|
|
98
|
+
communicationScore,
|
|
99
|
+
qualityPack,
|
|
100
|
+
autonomyPack,
|
|
101
|
+
judgmentPack,
|
|
102
|
+
communicationPack,
|
|
103
|
+
qualityConfidence,
|
|
104
|
+
autonomyConfidence,
|
|
105
|
+
judgmentConfidence,
|
|
106
|
+
communicationConfidence,
|
|
107
|
+
suggestedTrustPolicy: suggestTrustPolicy(qualityPack, autonomyPack),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function onboardingToRecommendation(result) {
|
|
111
|
+
const avgConfidence = (result.qualityConfidence + result.autonomyConfidence + result.judgmentConfidence + result.communicationConfidence) / 4;
|
|
112
|
+
return createRecommendation({
|
|
113
|
+
source: 'onboarding',
|
|
114
|
+
quality_pack: result.qualityPack,
|
|
115
|
+
autonomy_pack: result.autonomyPack,
|
|
116
|
+
judgment_pack: result.judgmentPack,
|
|
117
|
+
communication_pack: result.communicationPack,
|
|
118
|
+
suggested_trust_policy: result.suggestedTrustPolicy,
|
|
119
|
+
confidence: avgConfidence,
|
|
120
|
+
reason_summary: `온보딩 4문항 결과: quality=${result.qualityScore}, autonomy=${result.autonomyScore}, judgment=${result.judgmentScore}, communication=${result.communicationScore}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Compound Reflection Logic
|
|
3
|
+
*
|
|
4
|
+
* Code Reflection의 false positive를 줄이기 위한 3중 필터:
|
|
5
|
+
* 1. 시간 윈도우: 솔루션 주입 후 15분 이내만 반영으로 인정
|
|
6
|
+
* 2. 매칭 비율: 유효 식별자의 50% 이상 매칭 필요
|
|
7
|
+
* 3. 공통 식별자 차단: 프레임워크 기본 용어는 매칭에서 제외
|
|
8
|
+
*
|
|
9
|
+
* ADR: 기존 pre-tool-use.ts의 checkCompoundReflection 인라인 로직을
|
|
10
|
+
* 별도 모듈로 분리. 이유: (1) 테스트 가능성 (순수 함수), (2) false
|
|
11
|
+
* positive 문제(action-plan §2.1)의 근본 수정에 명확한 책임 경계 필요.
|
|
12
|
+
*/
|
|
13
|
+
/** 주입 후 이 시간 내에 코드에 식별자가 출현해야 reflection으로 인정 */
|
|
14
|
+
export declare const REFLECTION_WINDOW_MS: number;
|
|
15
|
+
/**
|
|
16
|
+
* 프레임워크/라이브러리 기본 식별자 블록리스트.
|
|
17
|
+
* 이 식별자들은 솔루션 주입과 무관하게 코드에 자연 출현하므로
|
|
18
|
+
* reflection 매칭에서 제외한다.
|
|
19
|
+
*
|
|
20
|
+
* 기준: "이 단어가 코드에 있다고 해서 사용자가 forgen의 솔루션을
|
|
21
|
+
* 참고했다고 볼 수 없는 단어"
|
|
22
|
+
*/
|
|
23
|
+
export declare const COMMON_IDENTIFIERS: Set<string>;
|
|
24
|
+
export interface ReflectionInput {
|
|
25
|
+
identifiers: string[];
|
|
26
|
+
code: string;
|
|
27
|
+
injectedAt: string;
|
|
28
|
+
now?: Date;
|
|
29
|
+
}
|
|
30
|
+
export interface ReflectionResult {
|
|
31
|
+
reflected: boolean;
|
|
32
|
+
matchedCount: number;
|
|
33
|
+
eligibleCount: number;
|
|
34
|
+
reason?: 'outside-window' | 'low-match-ratio' | 'no-eligible-identifiers' | 'code-too-short' | 'invalid-injection-time';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 솔루션의 식별자가 코드에 반영되었는지 판정한다 (순수 함수).
|
|
38
|
+
*
|
|
39
|
+
* 3중 필터:
|
|
40
|
+
* 1. 코드 최소 길이 (10자)
|
|
41
|
+
* 2. 시간 윈도우 (주입 후 15분)
|
|
42
|
+
* 3. 유효 식별자 필터링 (6자 이상 + 블록리스트 제외)
|
|
43
|
+
* 4. 매칭 비율 (유효 식별자의 50% 이상, 최소 1개)
|
|
44
|
+
*/
|
|
45
|
+
export declare function isReflectionCandidate(input: ReflectionInput): ReflectionResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Compound Reflection Logic
|
|
3
|
+
*
|
|
4
|
+
* Code Reflection의 false positive를 줄이기 위한 3중 필터:
|
|
5
|
+
* 1. 시간 윈도우: 솔루션 주입 후 15분 이내만 반영으로 인정
|
|
6
|
+
* 2. 매칭 비율: 유효 식별자의 50% 이상 매칭 필요
|
|
7
|
+
* 3. 공통 식별자 차단: 프레임워크 기본 용어는 매칭에서 제외
|
|
8
|
+
*
|
|
9
|
+
* ADR: 기존 pre-tool-use.ts의 checkCompoundReflection 인라인 로직을
|
|
10
|
+
* 별도 모듈로 분리. 이유: (1) 테스트 가능성 (순수 함수), (2) false
|
|
11
|
+
* positive 문제(action-plan §2.1)의 근본 수정에 명확한 책임 경계 필요.
|
|
12
|
+
*/
|
|
13
|
+
/** 주입 후 이 시간 내에 코드에 식별자가 출현해야 reflection으로 인정 */
|
|
14
|
+
export const REFLECTION_WINDOW_MS = 15 * 60 * 1000; // 15분
|
|
15
|
+
/**
|
|
16
|
+
* 프레임워크/라이브러리 기본 식별자 블록리스트.
|
|
17
|
+
* 이 식별자들은 솔루션 주입과 무관하게 코드에 자연 출현하므로
|
|
18
|
+
* reflection 매칭에서 제외한다.
|
|
19
|
+
*
|
|
20
|
+
* 기준: "이 단어가 코드에 있다고 해서 사용자가 forgen의 솔루션을
|
|
21
|
+
* 참고했다고 볼 수 없는 단어"
|
|
22
|
+
*/
|
|
23
|
+
export const COMMON_IDENTIFIERS = new Set([
|
|
24
|
+
// React
|
|
25
|
+
'useState', 'useEffect', 'useCallback', 'useReducer', 'useContext', 'useLayoutEffect',
|
|
26
|
+
'useImperativeHandle', 'useDebugValue', 'useDeferredValue', 'useTransition', 'useSyncExternalStore',
|
|
27
|
+
'useInsertionEffect', 'createElement', 'createContext', 'createRef', 'forwardRef',
|
|
28
|
+
'ErrorBoundary', 'Suspense', 'StrictMode', 'Fragment', 'Component',
|
|
29
|
+
// Next.js
|
|
30
|
+
'getServerSideProps', 'getStaticProps', 'getStaticPaths', 'NextRequest', 'NextResponse',
|
|
31
|
+
'useRouter', 'usePathname', 'useSearchParams',
|
|
32
|
+
// Node.js / common
|
|
33
|
+
'require', 'exports', 'module', 'process', 'console', 'setTimeout', 'setInterval',
|
|
34
|
+
'Promise', 'Buffer', 'EventEmitter',
|
|
35
|
+
// Testing
|
|
36
|
+
'describe', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll',
|
|
37
|
+
// TypeScript
|
|
38
|
+
'interface', 'implements', 'extends', 'abstract', 'readonly',
|
|
39
|
+
// Common methods/patterns
|
|
40
|
+
'toString', 'valueOf', 'constructor', 'prototype', 'hasOwnProperty',
|
|
41
|
+
'addEventListener', 'removeEventListener', 'querySelector', 'getElementById',
|
|
42
|
+
// Libraries
|
|
43
|
+
'express', 'mongoose', 'sequelize', 'prisma',
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* 솔루션의 식별자가 코드에 반영되었는지 판정한다 (순수 함수).
|
|
47
|
+
*
|
|
48
|
+
* 3중 필터:
|
|
49
|
+
* 1. 코드 최소 길이 (10자)
|
|
50
|
+
* 2. 시간 윈도우 (주입 후 15분)
|
|
51
|
+
* 3. 유효 식별자 필터링 (6자 이상 + 블록리스트 제외)
|
|
52
|
+
* 4. 매칭 비율 (유효 식별자의 50% 이상, 최소 1개)
|
|
53
|
+
*/
|
|
54
|
+
export function isReflectionCandidate(input) {
|
|
55
|
+
const { identifiers, code, injectedAt } = input;
|
|
56
|
+
const now = input.now ?? new Date();
|
|
57
|
+
// Gate 1: 코드 최소 길이
|
|
58
|
+
if (!code || code.length < 10) {
|
|
59
|
+
return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'code-too-short' };
|
|
60
|
+
}
|
|
61
|
+
// Gate 2: 주입 시각 유효성 + 시간 윈도우
|
|
62
|
+
const injectedTime = new Date(injectedAt).getTime();
|
|
63
|
+
if (Number.isNaN(injectedTime)) {
|
|
64
|
+
return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'invalid-injection-time' };
|
|
65
|
+
}
|
|
66
|
+
const elapsed = now.getTime() - injectedTime;
|
|
67
|
+
if (elapsed > REFLECTION_WINDOW_MS || elapsed < 0) {
|
|
68
|
+
return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'outside-window' };
|
|
69
|
+
}
|
|
70
|
+
// Gate 3: 유효 식별자 필터링
|
|
71
|
+
const eligible = identifiers.filter(id => id.length >= 6 && !COMMON_IDENTIFIERS.has(id));
|
|
72
|
+
if (eligible.length === 0) {
|
|
73
|
+
return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'no-eligible-identifiers' };
|
|
74
|
+
}
|
|
75
|
+
// Gate 4: 매칭 비율 검사
|
|
76
|
+
const matchedCount = eligible.filter(id => code.includes(id)).length;
|
|
77
|
+
const minRequired = Math.max(1, Math.ceil(eligible.length * 0.5));
|
|
78
|
+
if (matchedCount < minRequired) {
|
|
79
|
+
return { reflected: false, matchedCount, eligibleCount: eligible.length, reason: 'low-match-ratio' };
|
|
80
|
+
}
|
|
81
|
+
return { reflected: true, matchedCount, eligibleCount: eligible.length };
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Context Guard Hook
|
|
4
|
+
*
|
|
5
|
+
* Claude Code Stop 훅으로 등록.
|
|
6
|
+
* context window limit, edit error 등 실행 중 에러를 감지하여
|
|
7
|
+
* 사용자에게 경고하고 상태를 보존합니다.
|
|
8
|
+
*
|
|
9
|
+
* 또한 UserPromptSubmit에서 현재 대화 길이를 추적하여
|
|
10
|
+
* context 한계에 접근 시 preemptive 경고를 제공합니다.
|
|
11
|
+
*/
|
|
12
|
+
/** 경고 표시 여부 판정 (순수 함수) */
|
|
13
|
+
export declare function shouldWarn(contextPercent: {
|
|
14
|
+
promptCount: number;
|
|
15
|
+
totalChars: number;
|
|
16
|
+
lastWarningAt: number;
|
|
17
|
+
}, thresholds?: {
|
|
18
|
+
promptThreshold?: number;
|
|
19
|
+
charsThreshold?: number;
|
|
20
|
+
cooldownMs?: number;
|
|
21
|
+
}): boolean;
|
|
22
|
+
/** 경고 메시지 생성 (순수 함수) */
|
|
23
|
+
export declare function buildContextWarningMessage(promptCount: number, totalChars: number): string;
|
|
24
|
+
export declare function main(): Promise<void>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Context Guard Hook
|
|
4
|
+
*
|
|
5
|
+
* Claude Code Stop 훅으로 등록.
|
|
6
|
+
* context window limit, edit error 등 실행 중 에러를 감지하여
|
|
7
|
+
* 사용자에게 경고하고 상태를 보존합니다.
|
|
8
|
+
*
|
|
9
|
+
* 또한 UserPromptSubmit에서 현재 대화 길이를 추적하여
|
|
10
|
+
* context 한계에 접근 시 preemptive 경고를 제공합니다.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { createLogger } from '../core/logger.js';
|
|
16
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
17
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
18
|
+
import { loadHookConfig, isHookEnabled } from './hook-config.js';
|
|
19
|
+
import { approve, approveWithContext, approveWithWarning, failOpen } from './shared/hook-response.js';
|
|
20
|
+
import { HANDOFFS_DIR, STATE_DIR } from '../core/paths.js';
|
|
21
|
+
const log = createLogger('context-guard');
|
|
22
|
+
const CONTEXT_STATE_PATH = path.join(STATE_DIR, 'context-guard.json');
|
|
23
|
+
// 경고 임계값: 프롬프트 50회 또는 총 문자 수 200K 이상
|
|
24
|
+
const PROMPT_WARNING_THRESHOLD = 50;
|
|
25
|
+
const CHARS_WARNING_THRESHOLD = 200_000;
|
|
26
|
+
const WARNING_COOLDOWN_MS = 10 * 60 * 1000; // 10분 쿨다운
|
|
27
|
+
/** 경고 표시 여부 판정 (순수 함수) */
|
|
28
|
+
export function shouldWarn(contextPercent, thresholds = {}) {
|
|
29
|
+
const promptThreshold = thresholds.promptThreshold ?? PROMPT_WARNING_THRESHOLD;
|
|
30
|
+
const charsThreshold = thresholds.charsThreshold ?? CHARS_WARNING_THRESHOLD;
|
|
31
|
+
const cooldownMs = thresholds.cooldownMs ?? WARNING_COOLDOWN_MS;
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
return ((contextPercent.promptCount >= promptThreshold || contextPercent.totalChars >= charsThreshold) &&
|
|
34
|
+
(now - contextPercent.lastWarningAt > cooldownMs));
|
|
35
|
+
}
|
|
36
|
+
/** 경고 메시지 생성 (순수 함수) */
|
|
37
|
+
export function buildContextWarningMessage(promptCount, totalChars) {
|
|
38
|
+
return `<compound-context-warning>\n[Forgen] Context limit approaching: ${promptCount} prompts, ${Math.round(totalChars / 1000)}K characters.\nIf you have important progress, save it now:\n- Use cancelforgen to reset mode state and start a new session\n- Or continue current work (auto compaction may occur)\n</compound-context-warning>`;
|
|
39
|
+
}
|
|
40
|
+
function loadContextState(sessionId) {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(CONTEXT_STATE_PATH)) {
|
|
43
|
+
const data = JSON.parse(fs.readFileSync(CONTEXT_STATE_PATH, 'utf-8'));
|
|
44
|
+
if (data.sessionId === sessionId)
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
log.debug('context state 파일 읽기/파싱 실패', e);
|
|
50
|
+
}
|
|
51
|
+
return { promptCount: 0, totalChars: 0, lastWarningAt: 0, sessionId };
|
|
52
|
+
}
|
|
53
|
+
function saveContextState(state) {
|
|
54
|
+
atomicWriteJSON(CONTEXT_STATE_PATH, state);
|
|
55
|
+
}
|
|
56
|
+
export async function main() {
|
|
57
|
+
const input = await readStdinJSON();
|
|
58
|
+
if (!isHookEnabled('context-guard')) {
|
|
59
|
+
console.log(approve());
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!input) {
|
|
63
|
+
console.log(approve());
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const sessionId = input.session_id ?? 'default';
|
|
67
|
+
// Stop 훅: stop_hook_type이 있으면 처리
|
|
68
|
+
if (input.stop_hook_type) {
|
|
69
|
+
// 에러가 포함된 경우: context limit 감지
|
|
70
|
+
if (input.error) {
|
|
71
|
+
const errorMsg = input.error;
|
|
72
|
+
if (/context.*limit|token.*limit|conversation.*too.*long/i.test(errorMsg)) {
|
|
73
|
+
saveHandoff(sessionId, 'context-limit', errorMsg);
|
|
74
|
+
console.log(approveWithWarning(`[Forgen] Context limit reached. Current state has been saved to ~/.forgen/handoffs/.\nThe previous work will be automatically recovered in the next session.`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 정상 종료 시: 의미 있는 세션이었으면 compound 안내
|
|
79
|
+
if (input.stop_hook_type === 'user' || input.stop_hook_type === 'end_turn') {
|
|
80
|
+
const state = loadContextState(sessionId);
|
|
81
|
+
if (state.promptCount >= 10) {
|
|
82
|
+
// 10 프롬프트 이상이면 의미 있는 세션 — compound 안내
|
|
83
|
+
console.log(approveWithWarning(`[Forgen] 이 세션에서 ${state.promptCount}개의 프롬프트를 처리했습니다. /compound 를 실행하면 이 세션의 학습 내용을 축적할 수 있습니다.`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
console.log(approve());
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// error만 있는 경우 (stop_hook_type 없이)
|
|
91
|
+
if (input.error) {
|
|
92
|
+
console.log(approve());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// UserPromptSubmit 훅: 대화 길이 추적
|
|
96
|
+
if (input.prompt) {
|
|
97
|
+
const config = loadHookConfig('context-guard');
|
|
98
|
+
// maxTokens가 설정되어 있으면 chars threshold로 사용 (토큰 ≈ 4자 기준 환산)
|
|
99
|
+
const charsThreshold = typeof config?.maxTokens === 'number' ? config.maxTokens * 4 : undefined;
|
|
100
|
+
const state = loadContextState(sessionId);
|
|
101
|
+
state.promptCount++;
|
|
102
|
+
state.totalChars += input.prompt.length;
|
|
103
|
+
if (shouldWarn(state, charsThreshold !== undefined ? { charsThreshold } : {})) {
|
|
104
|
+
state.lastWarningAt = Date.now();
|
|
105
|
+
saveContextState(state);
|
|
106
|
+
console.log(approveWithContext(buildContextWarningMessage(state.promptCount, state.totalChars), 'UserPromptSubmit'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
saveContextState(state);
|
|
110
|
+
}
|
|
111
|
+
console.log(approve());
|
|
112
|
+
}
|
|
113
|
+
function saveHandoff(sessionId, reason, detail) {
|
|
114
|
+
fs.mkdirSync(HANDOFFS_DIR, { recursive: true });
|
|
115
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
116
|
+
const handoffPath = path.join(HANDOFFS_DIR, `${timestamp}-${reason}.md`);
|
|
117
|
+
// 활성 모드 상태 수집
|
|
118
|
+
const stateDir = STATE_DIR;
|
|
119
|
+
const activeStates = [];
|
|
120
|
+
if (fs.existsSync(stateDir)) {
|
|
121
|
+
for (const f of fs.readdirSync(stateDir)) {
|
|
122
|
+
if (f.endsWith('-state.json') && !f.startsWith('skill-cache-') && !f.startsWith('context-guard')) {
|
|
123
|
+
try {
|
|
124
|
+
const data = JSON.parse(fs.readFileSync(path.join(stateDir, f), 'utf-8'));
|
|
125
|
+
if (data.active) {
|
|
126
|
+
activeStates.push(`- ${f.replace('-state.json', '')}: ${data.prompt ?? 'no prompt'}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
log.debug(`상태 파일 파싱 실패: ${f}`, e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const content = [
|
|
136
|
+
`# Handoff: ${reason}`,
|
|
137
|
+
`- Session: ${sessionId}`,
|
|
138
|
+
`- Time: ${new Date().toISOString()}`,
|
|
139
|
+
`- Reason: ${detail}`,
|
|
140
|
+
'',
|
|
141
|
+
'## Active Modes',
|
|
142
|
+
activeStates.length > 0 ? activeStates.join('\n') : '- none',
|
|
143
|
+
'',
|
|
144
|
+
'## Recovery Instructions',
|
|
145
|
+
'Automatically recovered in the next session (session-recovery hook).',
|
|
146
|
+
'Manual recovery: Check the last state of the previous work and continue from there.',
|
|
147
|
+
].join('\n');
|
|
148
|
+
fs.writeFileSync(handoffPath, content);
|
|
149
|
+
}
|
|
150
|
+
// ESM main guard: import 시 main() 실행 방지
|
|
151
|
+
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
152
|
+
main().catch((e) => {
|
|
153
|
+
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
154
|
+
console.log(failOpen());
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "pattern": "rm\\s+(-rf|-fr)\\s+[/~]", "description": "rm -rf on root/home path", "severity": "block" },
|
|
3
|
+
{ "pattern": "rm\\s+(-rf|-fr)\\s+\\.\\s", "description": "rm -rf on current directory", "severity": "block" },
|
|
4
|
+
{ "pattern": "git\\s+push\\s+.*--force(?!-)", "description": "git push --force", "severity": "warn" },
|
|
5
|
+
{ "pattern": "git\\s+reset\\s+--hard", "description": "git reset --hard", "severity": "warn" },
|
|
6
|
+
{ "pattern": "git\\s+clean\\s+-[a-z]*f", "description": "git clean -f", "severity": "warn" },
|
|
7
|
+
{ "pattern": "drop\\s+(?:table|database)", "description": "DROP TABLE/DATABASE", "severity": "block", "flags": "i" },
|
|
8
|
+
{ "pattern": "truncate\\s+table", "description": "TRUNCATE TABLE", "severity": "warn", "flags": "i" },
|
|
9
|
+
{ "pattern": ">\\s*\\/dev\\/sd[a-z]", "description": "write to block device", "severity": "block" },
|
|
10
|
+
{ "pattern": "mkfs\\s", "description": "mkfs (format filesystem)", "severity": "block" },
|
|
11
|
+
{ "pattern": ":\\(\\)\\s*\\{\\s*:\\|:&\\s*\\}\\s*;:", "description": "fork bomb", "severity": "block" },
|
|
12
|
+
{ "pattern": "\\beval\\s+[\"'`]", "description": "eval with string (injection risk)", "severity": "warn" },
|
|
13
|
+
{ "pattern": "curl\\s+.*\\|\\s*(ba)?sh", "description": "curl pipe to shell", "severity": "block" },
|
|
14
|
+
{ "pattern": "wget\\s+.*\\|\\s*(ba)?sh", "description": "wget pipe to shell", "severity": "block" },
|
|
15
|
+
{ "pattern": "python[23]?\\s+-c\\s+['\"].*(?:import\\s+os|subprocess|exec|eval)", "description": "python -c with dangerous imports", "severity": "warn" },
|
|
16
|
+
{ "pattern": "\\bchmod\\s+[0-7]*777\\b", "description": "chmod 777 (overly permissive)", "severity": "warn" },
|
|
17
|
+
{ "pattern": "\\bdd\\s+.*of=\\/dev\\/", "description": "dd write to device", "severity": "block" }
|
|
18
|
+
]
|