@wooojin/forgen 0.2.1 → 0.3.1
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/CHANGELOG.md +76 -0
- package/README.ko.md +25 -14
- package/README.md +61 -17
- package/agents/analyst.md +48 -4
- package/agents/architect.md +39 -4
- package/agents/code-reviewer.md +107 -77
- package/agents/critic.md +47 -4
- package/agents/debugger.md +46 -4
- package/agents/designer.md +40 -4
- package/agents/executor.md +112 -30
- package/agents/explore.md +45 -5
- package/agents/git-master.md +48 -4
- package/agents/planner.md +121 -18
- package/agents/solution-evolver.md +115 -0
- package/agents/test-engineer.md +58 -4
- package/agents/verifier.md +92 -77
- package/commands/architecture-decision.md +127 -258
- package/commands/calibrate.md +225 -0
- package/commands/code-review.md +163 -178
- package/commands/compound.md +127 -68
- package/commands/deep-interview.md +212 -110
- package/commands/docker.md +68 -178
- package/commands/forge-loop.md +215 -0
- package/commands/learn.md +231 -0
- package/commands/retro.md +215 -0
- package/commands/ship.md +277 -0
- package/dist/cli.js +25 -9
- package/dist/core/auto-compound-runner.js +14 -0
- package/dist/core/config-injector.d.ts +2 -1
- package/dist/core/config-injector.js +2 -1
- package/dist/core/dashboard.d.ts +17 -0
- package/dist/core/dashboard.js +158 -2
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/paths.d.ts +31 -1
- package/dist/core/paths.js +43 -2
- package/dist/core/spawn.d.ts +3 -2
- package/dist/core/spawn.js +27 -8
- package/dist/core/types.d.ts +34 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/learn-cli.d.ts +1 -0
- package/dist/engine/learn-cli.js +182 -0
- package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
- package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
- package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
- package/dist/engine/meta-learning/extraction-tuner.js +99 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
- package/dist/engine/meta-learning/runner.d.ts +14 -0
- package/dist/engine/meta-learning/runner.js +90 -0
- package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
- package/dist/engine/meta-learning/scope-promoter.js +84 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
- package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
- package/dist/engine/meta-learning/types.d.ts +114 -0
- package/dist/engine/meta-learning/types.js +43 -0
- package/dist/engine/solution-candidate.d.ts +30 -0
- package/dist/engine/solution-candidate.js +124 -0
- package/dist/engine/solution-fitness.d.ts +52 -0
- package/dist/engine/solution-fitness.js +95 -0
- package/dist/engine/solution-fixup.d.ts +30 -0
- package/dist/engine/solution-fixup.js +116 -0
- package/dist/engine/solution-format.d.ts +10 -2
- package/dist/engine/solution-format.js +287 -57
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-index.js +10 -0
- package/dist/engine/solution-matcher.d.ts +7 -1
- package/dist/engine/solution-matcher.js +137 -37
- package/dist/engine/solution-outcomes.d.ts +70 -0
- package/dist/engine/solution-outcomes.js +242 -0
- package/dist/engine/solution-quarantine.d.ts +36 -0
- package/dist/engine/solution-quarantine.js +172 -0
- package/dist/engine/solution-weakness.d.ts +45 -0
- package/dist/engine/solution-weakness.js +225 -0
- package/dist/engine/solution-writer.d.ts +5 -0
- package/dist/engine/solution-writer.js +18 -0
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +5 -0
- package/dist/hooks/context-guard.js +118 -2
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/keyword-detector.js +16 -100
- package/dist/hooks/post-tool-failure.js +7 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +6 -4
- package/dist/hooks/solution-injector.js +20 -0
- package/dist/host/codex-adapter.d.ts +10 -0
- package/dist/host/codex-adapter.js +154 -0
- package/dist/mcp/solution-reader.d.ts +5 -5
- package/dist/mcp/solution-reader.js +34 -24
- package/dist/mcp/tools.js +8 -0
- package/dist/services/session.d.ts +19 -0
- package/dist/services/session.js +62 -0
- package/hooks/hooks.json +2 -2
- package/package.json +2 -1
- package/skills/architecture-decision/SKILL.md +113 -257
- package/skills/calibrate/SKILL.md +207 -0
- package/skills/code-review/SKILL.md +151 -178
- package/skills/compound/SKILL.md +126 -68
- package/skills/deep-interview/SKILL.md +210 -110
- package/skills/docker/SKILL.md +57 -179
- package/skills/forge-loop/SKILL.md +198 -0
- package/skills/learn/SKILL.md +216 -0
- package/skills/retro/SKILL.md +199 -0
- package/skills/ship/SKILL.md +259 -0
- package/agents/code-simplifier.md +0 -197
- package/agents/performance-reviewer.md +0 -172
- package/agents/qa-tester.md +0 -158
- package/agents/refactoring-expert.md +0 -168
- package/agents/scientist.md +0 -144
- package/agents/security-reviewer.md +0 -137
- package/agents/writer.md +0 -184
- package/commands/api-design.md +0 -268
- package/commands/ci-cd.md +0 -270
- package/commands/database.md +0 -263
- package/commands/debug-detective.md +0 -99
- package/commands/documentation.md +0 -276
- package/commands/ecomode.md +0 -51
- package/commands/frontend.md +0 -271
- package/commands/git-master.md +0 -90
- package/commands/incident-response.md +0 -292
- package/commands/migrate.md +0 -101
- package/commands/performance.md +0 -288
- package/commands/refactor.md +0 -105
- package/commands/security-review.md +0 -288
- package/commands/specify.md +0 -128
- package/commands/tdd.md +0 -183
- package/commands/testing-strategy.md +0 -265
- package/skills/api-design/SKILL.md +0 -262
- package/skills/ci-cd/SKILL.md +0 -264
- package/skills/database/SKILL.md +0 -257
- package/skills/debug-detective/SKILL.md +0 -95
- package/skills/documentation/SKILL.md +0 -270
- package/skills/ecomode/SKILL.md +0 -46
- package/skills/frontend/SKILL.md +0 -265
- package/skills/git-master/SKILL.md +0 -86
- package/skills/incident-response/SKILL.md +0 -286
- package/skills/migrate/SKILL.md +0 -96
- package/skills/performance/SKILL.md +0 -282
- package/skills/refactor/SKILL.md +0 -100
- package/skills/security-review/SKILL.md +0 -282
- package/skills/specify/SKILL.md +0 -122
- package/skills/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - forgen config hooks (사용자 설정 변경 후)
|
|
10
10
|
* - forgen install (플러그인 설치 후)
|
|
11
11
|
*/
|
|
12
|
+
import { type RuntimeHost } from '../core/types.js';
|
|
12
13
|
interface HookCommand {
|
|
13
14
|
type: 'command';
|
|
14
15
|
command: string;
|
|
@@ -27,6 +28,8 @@ interface GenerateOptions {
|
|
|
27
28
|
cwd?: string;
|
|
28
29
|
/** 훅 실행 스크립트의 루트 경로 */
|
|
29
30
|
pluginRoot?: string;
|
|
31
|
+
/** 런타임 (claude|codex) */
|
|
32
|
+
runtime?: RuntimeHost;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* 활성 훅만 포함한 hooks.json 객체를 생성합니다.
|
|
@@ -14,6 +14,27 @@ import * as path from 'node:path';
|
|
|
14
14
|
import { HOOK_REGISTRY } from './hook-registry.js';
|
|
15
15
|
import { isHookEnabled } from './hook-config.js';
|
|
16
16
|
import { detectInstalledPlugins, getHookConflicts } from '../core/plugin-detector.js';
|
|
17
|
+
function splitCommand(raw) {
|
|
18
|
+
const tokens = raw.match(/"([^"]+)"|\S+/g) ?? [];
|
|
19
|
+
const unquoted = tokens.map(token => token.replace(/^"/, '').replace(/"$/, ''));
|
|
20
|
+
return { script: unquoted[0] ?? '', args: unquoted.slice(1) };
|
|
21
|
+
}
|
|
22
|
+
function quoteArg(raw) {
|
|
23
|
+
return `"${raw.replace(/"/g, '\\"')}"`;
|
|
24
|
+
}
|
|
25
|
+
function buildHookCommand(pluginRoot, rawScript, runtime) {
|
|
26
|
+
const { script, args } = splitCommand(rawScript);
|
|
27
|
+
const scriptPath = `${pluginRoot}/${script}`;
|
|
28
|
+
const quotedArgs = args.map(quoteArg).join(' ');
|
|
29
|
+
if (runtime === 'codex') {
|
|
30
|
+
const adapterPath = `${pluginRoot}/host/codex-adapter.js`;
|
|
31
|
+
const baseCommand = `node ${quoteArg(adapterPath)} ${quoteArg(scriptPath)}`;
|
|
32
|
+
return `${baseCommand}${quotedArgs ? ` ${quotedArgs}` : ''}`;
|
|
33
|
+
}
|
|
34
|
+
return quotedArgs
|
|
35
|
+
? `node ${quoteArg(scriptPath)} ${quotedArgs}`
|
|
36
|
+
: `node ${quoteArg(scriptPath)}`;
|
|
37
|
+
}
|
|
17
38
|
/**
|
|
18
39
|
* 활성 훅만 포함한 hooks.json 객체를 생성합니다.
|
|
19
40
|
*
|
|
@@ -27,6 +48,7 @@ export function generateHooksJson(options) {
|
|
|
27
48
|
const cwd = options?.cwd;
|
|
28
49
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: CLAUDE_PLUGIN_ROOT is a Claude Code Plugin SDK variable resolved at runtime
|
|
29
50
|
const pluginRoot = options?.pluginRoot ?? '${CLAUDE_PLUGIN_ROOT}/dist';
|
|
51
|
+
const runtime = options?.runtime ?? 'claude';
|
|
30
52
|
// 다른 플러그인의 충돌 훅 감지
|
|
31
53
|
const hookConflicts = getHookConflicts(cwd);
|
|
32
54
|
const detectedPlugins = detectInstalledPlugins(cwd);
|
|
@@ -64,12 +86,7 @@ export function generateHooksJson(options) {
|
|
|
64
86
|
hooks[event] = [...byMatcher.entries()].map(([matcher, matcherEntries]) => ({
|
|
65
87
|
matcher,
|
|
66
88
|
hooks: matcherEntries.map(h => {
|
|
67
|
-
|
|
68
|
-
// 파일 경로와 인자를 분리해야 셸에서 ENOENT를 방지
|
|
69
|
-
const spaceIdx = h.script.indexOf(' ');
|
|
70
|
-
const command = spaceIdx === -1
|
|
71
|
-
? `node "${pluginRoot}/${h.script}"`
|
|
72
|
-
: `node "${pluginRoot}/${h.script.slice(0, spaceIdx)}" ${h.script.slice(spaceIdx + 1)}`;
|
|
89
|
+
const command = buildHookCommand(pluginRoot, h.script, runtime);
|
|
73
90
|
return { type: 'command', command, timeout: h.timeout };
|
|
74
91
|
}),
|
|
75
92
|
}));
|
|
@@ -29,7 +29,7 @@ import { recordHookTiming } from './shared/hook-timing.js';
|
|
|
29
29
|
function escapeXmlAttr(s) {
|
|
30
30
|
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
31
31
|
}
|
|
32
|
-
const WORKFLOW_TRACKED_INJECTS = new Set(
|
|
32
|
+
const WORKFLOW_TRACKED_INJECTS = new Set();
|
|
33
33
|
export function shouldTrackWorkflowActivation(match) {
|
|
34
34
|
if (match.type === 'inject')
|
|
35
35
|
return WORKFLOW_TRACKED_INJECTS.has(match.keyword);
|
|
@@ -51,21 +51,17 @@ export const KEYWORD_PATTERNS = [
|
|
|
51
51
|
{ pattern: /\bccg\b/i, keyword: 'ccg', type: 'skill', skill: 'ccg' },
|
|
52
52
|
{ pattern: /\bralplan\b/i, keyword: 'ralplan', type: 'skill', skill: 'ralplan' },
|
|
53
53
|
{ pattern: /\bdeep[- ]?interview\b/i, keyword: 'deep-interview', type: 'skill', skill: 'deep-interview' },
|
|
54
|
-
{ pattern: /\bspecify\b|(?:^|\s)(명세|요구사항\s*정리|스펙\s*정리)(?:\s|$)/im, keyword: 'specify', type: 'skill', skill: 'specify' },
|
|
55
54
|
{ pattern: /\bpipeline\b/i, keyword: 'pipeline', type: 'skill', skill: 'pipeline' },
|
|
56
|
-
{ pattern: /\becomode\b|(?:^|\s)(에코\s*모드|토큰\s*절약)(?:\s|$)/im, keyword: 'ecomode', type: 'skill', skill: 'ecomode' },
|
|
57
55
|
// 인젝션 모드
|
|
58
56
|
{ pattern: /\bultrathink\b/i, keyword: 'ultrathink', type: 'inject' },
|
|
59
57
|
{ pattern: /\bdeepsearch\b/i, keyword: 'deepsearch', type: 'inject' },
|
|
60
|
-
{ pattern: /(?:^|\s)tdd(?:\s+(?:모드|mode|방식|으로|해|해줘|시작|적용)|\s*$)/im, keyword: 'tdd', type: 'skill', skill: 'tdd' },
|
|
61
58
|
{ pattern: /(?:code[- ]?review|코드\s*리뷰)\s*(?:해|해줘|시작|해봐|부탁|mode|모드)/i, keyword: 'code-review', type: 'skill', skill: 'code-review' },
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{ pattern:
|
|
65
|
-
{ pattern: /\b
|
|
66
|
-
{ pattern:
|
|
67
|
-
{ pattern: /\
|
|
68
|
-
{ pattern: /\b(refactor)\s*(?:mode|모드|해|해줘|시작|실행|진행)|(?:^|\s)(리팩토링|리팩터)\s*(?:mode|모드|해|해줘|시작|실행|진행)/im, keyword: 'refactor', type: 'skill', skill: 'refactor' },
|
|
59
|
+
// forgen 핵심 스킬
|
|
60
|
+
{ pattern: /\b(forge[- ]?loop|포지[- ]?루프)\b|(?:^|\s)(끝까지|don'?t\s*stop)(?:\s|$)/im, keyword: 'forge-loop', type: 'skill', skill: 'forge-loop' },
|
|
61
|
+
{ pattern: /(?:^|\s)ship(?:\s|$)|(?:^|\s)(배포|릴리스)\s*(?:해|해줘|하자|시작|진행)/im, keyword: 'ship', type: 'skill', skill: 'ship' },
|
|
62
|
+
{ pattern: /\bretro\b|(?:^|\s)(회고|돌아보기)(?:\s|$)/im, keyword: 'retro', type: 'skill', skill: 'retro' },
|
|
63
|
+
{ pattern: /(?:^|\s)learn\s+(?:search|prune|stats|export)|(?:^|\s)(학습\s*관리|compound\s*정리|솔루션\s*정리)/im, keyword: 'learn', type: 'skill', skill: 'learn' },
|
|
64
|
+
{ pattern: /\bcalibrate\b|(?:^|\s)(캘리브|프로필\s*보정|프로필\s*조정|프로필\s*확인)(?:\s|$)/im, keyword: 'calibrate', type: 'skill', skill: 'calibrate' },
|
|
69
65
|
];
|
|
70
66
|
// ── 인젝션 메시지 ──
|
|
71
67
|
const INJECT_MESSAGES = {
|
|
@@ -84,98 +80,12 @@ Perform comprehensive codebase exploration before answering:
|
|
|
84
80
|
4. Cross-reference findings across files
|
|
85
81
|
5. Present a complete, evidence-based analysis
|
|
86
82
|
</compound-deepsearch>`,
|
|
87
|
-
tdd: `<compound-tdd>
|
|
88
|
-
TDD MODE ACTIVATED.
|
|
89
|
-
Follow strict Test-Driven Development:
|
|
90
|
-
1. Write the failing test FIRST (Red)
|
|
91
|
-
2. Write the minimum code to pass (Green)
|
|
92
|
-
3. Refactor while keeping tests green (Refactor)
|
|
93
|
-
4. Repeat for each requirement
|
|
94
|
-
Never write implementation before tests.
|
|
95
|
-
</compound-tdd>`,
|
|
96
|
-
'code-review': `<compound-code-review>
|
|
97
|
-
CODE REVIEW MODE ACTIVATED.
|
|
98
|
-
Perform thorough code review with severity ratings:
|
|
99
|
-
- 🔴 CRITICAL: Security vulnerabilities, data loss risks, crashes
|
|
100
|
-
- 🟡 MAJOR: Logic errors, performance issues, missing error handling
|
|
101
|
-
- 🔵 MINOR: Style, naming, documentation improvements
|
|
102
|
-
- 💡 SUGGESTION: Optional enhancements
|
|
103
|
-
Provide file:line references for every finding.
|
|
104
|
-
</compound-code-review>`,
|
|
105
|
-
'security-review': `<compound-security-review>
|
|
106
|
-
SECURITY REVIEW MODE ACTIVATED.
|
|
107
|
-
Check for OWASP Top 10 and common vulnerabilities:
|
|
108
|
-
1. Injection (SQL, XSS, Command)
|
|
109
|
-
2. Broken Authentication / Authorization
|
|
110
|
-
3. Sensitive Data Exposure
|
|
111
|
-
4. Security Misconfiguration
|
|
112
|
-
5. Insecure Dependencies
|
|
113
|
-
6. Secrets in code (API keys, tokens, passwords)
|
|
114
|
-
7. Input validation gaps
|
|
115
|
-
8. Unsafe deserialization
|
|
116
|
-
Rate each finding: CRITICAL / HIGH / MEDIUM / LOW
|
|
117
|
-
</compound-security-review>`,
|
|
118
|
-
'git-master': `<compound-git-master>
|
|
119
|
-
GIT MASTER MODE ACTIVATED.
|
|
120
|
-
Apply atomic commit strategy and clean history management:
|
|
121
|
-
1. One commit = one logical change (atomic)
|
|
122
|
-
2. Follow Conventional Commits: feat/fix/refactor/docs/chore(<scope>): <subject>
|
|
123
|
-
3. Use interactive rebase (git rebase -i) to clean up WIP commits before pushing
|
|
124
|
-
4. Never force-push to shared branches (main, develop)
|
|
125
|
-
5. Use git bisect for systematic bug hunt across commits
|
|
126
|
-
Commit message format: <type>(<scope>): <subject> — imperative, 50 chars max
|
|
127
|
-
</compound-git-master>`,
|
|
128
|
-
benchmark: `<compound-benchmark>
|
|
129
|
-
BENCHMARK MODE ACTIVATED.
|
|
130
|
-
Measure performance with statistical rigor:
|
|
131
|
-
1. Collect baseline metrics FIRST (before any changes)
|
|
132
|
-
2. Run minimum 30 iterations (skip first 5 as warmup)
|
|
133
|
-
3. Calculate: avg, p95, p99, min, max
|
|
134
|
-
4. Measure: execution time (performance.now()), memory (process.memoryUsage()), bundle size
|
|
135
|
-
5. Output before/after comparison table with delta percentages
|
|
136
|
-
6. Use same environment for both measurements to ensure validity
|
|
137
|
-
</compound-benchmark>`,
|
|
138
|
-
migrate: `<compound-migrate>
|
|
139
|
-
MIGRATION MODE ACTIVATED.
|
|
140
|
-
Follow the 5-phase safe migration workflow:
|
|
141
|
-
1. ANALYZE: Document current state, identify breaking changes, map affected files
|
|
142
|
-
2. PLAN: Decompose into atomic steps, define rollback triggers (error rate > N%)
|
|
143
|
-
3. BACKUP: Create DB dump + git tag as restore point before any changes
|
|
144
|
-
4. EXECUTE: Apply Expand-Contract pattern for zero-downtime DB changes
|
|
145
|
-
5. VERIFY: Run E2E tests, check data integrity, validate performance regression
|
|
146
|
-
Rollback criteria: error rate spike, latency > 2x baseline, data inconsistency
|
|
147
|
-
</compound-migrate>`,
|
|
148
|
-
'debug-detective': `<compound-debug-detective>
|
|
149
|
-
DEBUG DETECTIVE MODE ACTIVATED.
|
|
150
|
-
Follow the Reproduce → Isolate → Fix → Verify loop:
|
|
151
|
-
1. REPRODUCE: Document exact conditions, input, expected vs actual, reproduction rate
|
|
152
|
-
2. ISOLATE: Classify error type (runtime/type/logic/async), use git bisect for regression
|
|
153
|
-
3. FIX: Address root cause (not symptoms), minimize change scope
|
|
154
|
-
4. VERIFY: Add regression test, confirm fix in staging before production
|
|
155
|
-
Error classification:
|
|
156
|
-
- Runtime: TypeError/ReferenceError → trace stack
|
|
157
|
-
- Logic: wrong output → add intermediate logging
|
|
158
|
-
- Async: race condition → check Promise chain, event ordering
|
|
159
|
-
Never guess — always reproduce first.
|
|
160
|
-
</compound-debug-detective>`,
|
|
161
|
-
refactor: `<compound-refactor>
|
|
162
|
-
REFACTOR MODE ACTIVATED.
|
|
163
|
-
Safe refactoring with test-first approach:
|
|
164
|
-
1. SECURE TESTS: Characterization tests for untested code before touching anything
|
|
165
|
-
2. IDENTIFY SMELLS: Long functions (>50 lines), duplication, deep nesting (>3), magic numbers
|
|
166
|
-
3. APPLY SOLID: Single responsibility, Open-closed, Liskov, Interface segregation, Dependency inversion
|
|
167
|
-
4. REFACTOR CATALOG: Extract Method, Move Method, Replace Conditional with Polymorphism
|
|
168
|
-
5. VERIFY: Run full test suite after each refactoring step
|
|
169
|
-
Rules:
|
|
170
|
-
- Never mix refactoring + feature changes in the same commit
|
|
171
|
-
- One refactoring pattern per commit
|
|
172
|
-
- Keep tests green at all times
|
|
173
|
-
</compound-refactor>`,
|
|
174
83
|
};
|
|
175
84
|
// ── 스킬 파일 로드 ──
|
|
176
85
|
function loadSkillContent(skillName) {
|
|
177
|
-
// 스킬 파일 검색 순서: 프로젝트 >
|
|
86
|
+
// 스킬 파일 검색 순서: 프로젝트 .forgen > 프로젝트 .compound > 팩 > 개인 > 글로벌 > 패키지 내장
|
|
178
87
|
const searchPaths = [
|
|
88
|
+
path.join(process.cwd(), '.forgen', 'skills', `${skillName}.md`),
|
|
179
89
|
path.join(process.cwd(), '.compound', 'skills', `${skillName}.md`),
|
|
180
90
|
path.join(process.cwd(), 'skills', `${skillName}.md`),
|
|
181
91
|
];
|
|
@@ -302,7 +212,7 @@ async function main() {
|
|
|
302
212
|
catch { /* 파일 없으면 무시 */ }
|
|
303
213
|
}
|
|
304
214
|
else {
|
|
305
|
-
// 모든 모드 상태 초기화 (ralplan, deep-interview 포함)
|
|
215
|
+
// 모든 모드 상태 초기화 (ralplan, deep-interview, forge-loop 등 포함)
|
|
306
216
|
for (const mode of ALL_MODES) {
|
|
307
217
|
clearState(`${mode}-state`);
|
|
308
218
|
}
|
|
@@ -311,6 +221,12 @@ async function main() {
|
|
|
311
221
|
fs.unlinkSync(ralphLoopState);
|
|
312
222
|
}
|
|
313
223
|
catch { /* 파일 없으면 무시 */ }
|
|
224
|
+
// forge-loop 상태 파일도 명시적으로 삭제 (Stop 훅 차단 해제)
|
|
225
|
+
const forgeLoopState = path.join(STATE_DIR, 'forge-loop.json');
|
|
226
|
+
try {
|
|
227
|
+
fs.unlinkSync(forgeLoopState);
|
|
228
|
+
}
|
|
229
|
+
catch { /* 파일 없으면 무시 */ }
|
|
314
230
|
}
|
|
315
231
|
// skill-cache 파일도 정리 (재주입 가능하도록)
|
|
316
232
|
cleanSkillCaches();
|
|
@@ -105,6 +105,13 @@ async function main() {
|
|
|
105
105
|
saveFailureState(state);
|
|
106
106
|
// 컨텍스트 신호 업데이트
|
|
107
107
|
incrementFailureSignal(sessionId);
|
|
108
|
+
// Outcome tracking (Phase 1): attribute this tool failure to pending
|
|
109
|
+
// solution injections in the same session. Fail-open.
|
|
110
|
+
try {
|
|
111
|
+
const { attributeError } = await import('../engine/solution-outcomes.js');
|
|
112
|
+
attributeError(sessionId);
|
|
113
|
+
}
|
|
114
|
+
catch { /* ignore */ }
|
|
108
115
|
const failCount = state.failures[toolName].count;
|
|
109
116
|
const suggestion = getRecoverySuggestion(error, toolName);
|
|
110
117
|
// 3회 이상 반복 실패 시 강화된 경고
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Claude Code UserPromptSubmit 훅으로 등록.
|
|
6
6
|
* 프롬프트와 매칭되는 학습된 스킬을 자동으로 컨텍스트에 주입합니다.
|
|
7
7
|
*
|
|
8
|
-
* 스킬 파일
|
|
8
|
+
* 스킬 파일 위치 (우선순위):
|
|
9
|
+
* 0. {project}/.forgen/skills/*.md (프로젝트 포지 스킬 — 최우선)
|
|
9
10
|
* 1. {project}/.compound/skills/*.md (프로젝트 스킬)
|
|
10
|
-
* 2. ~/.
|
|
11
|
-
* 3. ~/.
|
|
11
|
+
* 2. ~/.forgen/me/skills/*.md (개인 학습 스킬)
|
|
12
|
+
* 3. ~/.forgen/skills/*.md (글로벌 스킬)
|
|
12
13
|
*
|
|
13
14
|
* 스킬 포맷:
|
|
14
15
|
* ---
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Claude Code UserPromptSubmit 훅으로 등록.
|
|
6
6
|
* 프롬프트와 매칭되는 학습된 스킬을 자동으로 컨텍스트에 주입합니다.
|
|
7
7
|
*
|
|
8
|
-
* 스킬 파일
|
|
8
|
+
* 스킬 파일 위치 (우선순위):
|
|
9
|
+
* 0. {project}/.forgen/skills/*.md (프로젝트 포지 스킬 — 최우선)
|
|
9
10
|
* 1. {project}/.compound/skills/*.md (프로젝트 스킬)
|
|
10
|
-
* 2. ~/.
|
|
11
|
-
* 3. ~/.
|
|
11
|
+
* 2. ~/.forgen/me/skills/*.md (개인 학습 스킬)
|
|
12
|
+
* 3. ~/.forgen/skills/*.md (글로벌 스킬)
|
|
12
13
|
*
|
|
13
14
|
* 스킬 포맷:
|
|
14
15
|
* ---
|
|
@@ -181,8 +182,9 @@ function collectSkills() {
|
|
|
181
182
|
const seen = new Map(); // name → source dir
|
|
182
183
|
// 패키지 내장 스킬 경로 (dist/../skills/)
|
|
183
184
|
const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
|
|
184
|
-
//
|
|
185
|
+
// 프로젝트 .forgen > 프로젝트 .compound > 개인 > 글로벌 > 패키지 내장
|
|
185
186
|
const dirs = [
|
|
187
|
+
path.join(process.cwd(), '.forgen', 'skills'),
|
|
186
188
|
path.join(process.cwd(), '.compound', 'skills'),
|
|
187
189
|
path.join(ME_DIR, 'skills'),
|
|
188
190
|
path.join(FORGEN_HOME, 'skills'),
|
|
@@ -28,6 +28,7 @@ import { writeSignal } from './shared/plugin-signal.js';
|
|
|
28
28
|
import { approve, approveWithContext, failOpenWithTracking } from './shared/hook-response.js';
|
|
29
29
|
import { STATE_DIR } from '../core/paths.js';
|
|
30
30
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
31
|
+
import { appendPending, flushAccept } from '../engine/solution-outcomes.js';
|
|
31
32
|
const MAX_SOLUTIONS_PER_SESSION = 10;
|
|
32
33
|
/** 세션별 이미 주입된 솔루션 추적 (중복 방지) */
|
|
33
34
|
function getSessionCachePath(sessionId) {
|
|
@@ -451,6 +452,25 @@ async function main() {
|
|
|
451
452
|
catch (e) {
|
|
452
453
|
log.debug('plugin signal 기록 실패', e);
|
|
453
454
|
}
|
|
455
|
+
// Outcome tracking (Phase 1): flush previous pending as `accept` (silence
|
|
456
|
+
// = consent), then record this round's injections as new pending. Both
|
|
457
|
+
// calls are fail-open — a tracking crash must not block injection.
|
|
458
|
+
try {
|
|
459
|
+
flushAccept(sessionId);
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
log.debug('outcome flushAccept 실패', e);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
appendPending(sessionId, effectiveToInject.map((sol) => ({
|
|
466
|
+
solution: sol.name,
|
|
467
|
+
match_score: sol.relevance,
|
|
468
|
+
injected_chars: (summaries.get(sol.name) ?? sol.name).length,
|
|
469
|
+
})));
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
log.debug('outcome appendPending 실패', e);
|
|
473
|
+
}
|
|
454
474
|
console.log(approveWithContext(fullInjection, 'UserPromptSubmit'));
|
|
455
475
|
}
|
|
456
476
|
finally {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Codex 훅 어댑터
|
|
4
|
+
*
|
|
5
|
+
* 목적:
|
|
6
|
+
* - codex 런타임에서 실행되는 훅 스크립트 출력을 Claude Hook schema로 정규화
|
|
7
|
+
* - continue 누락 또는 codex 특화 판정 필드(approved/decision) 대응
|
|
8
|
+
* - 파싱 실패/실행 실패 시 fail-open(continue: true)
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
function parseDecision(raw) {
|
|
12
|
+
if (typeof raw === 'boolean') {
|
|
13
|
+
return { continueFlag: raw };
|
|
14
|
+
}
|
|
15
|
+
if (typeof raw === 'string') {
|
|
16
|
+
const normalized = raw.toLowerCase();
|
|
17
|
+
if (normalized === 'continue')
|
|
18
|
+
return { continueFlag: true };
|
|
19
|
+
if (normalized === 'stop' || normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
|
|
20
|
+
return { continueFlag: false, permissionDecision: normalized };
|
|
21
|
+
}
|
|
22
|
+
return { continueFlag: true };
|
|
23
|
+
}
|
|
24
|
+
if (typeof raw !== 'object' || raw === null)
|
|
25
|
+
return { continueFlag: true };
|
|
26
|
+
const value = raw.decision;
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
const normalized = value.toLowerCase();
|
|
29
|
+
if (normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
|
|
30
|
+
return { continueFlag: false, permissionDecision: normalized };
|
|
31
|
+
}
|
|
32
|
+
if (normalized === 'ask' || normalized === 'prompt' || normalized === 'confirm') {
|
|
33
|
+
return { continueFlag: true, permissionDecision: normalized };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (typeof raw.approved === 'boolean') {
|
|
37
|
+
const approved = raw.approved;
|
|
38
|
+
return approved
|
|
39
|
+
? { continueFlag: true, permissionDecision: raw.decision || 'approve' }
|
|
40
|
+
: { continueFlag: false, permissionDecision: 'deny' };
|
|
41
|
+
}
|
|
42
|
+
if (typeof raw.continue === 'boolean') {
|
|
43
|
+
return { continueFlag: raw.continue };
|
|
44
|
+
}
|
|
45
|
+
return { continueFlag: true };
|
|
46
|
+
}
|
|
47
|
+
function lastJSONObjectFromText(raw) {
|
|
48
|
+
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
49
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(lines[i]);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// continue
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function normalizeOutput(raw, input) {
|
|
65
|
+
const result = { continue: true };
|
|
66
|
+
const decision = parseDecision(raw);
|
|
67
|
+
result.continue = decision.continueFlag;
|
|
68
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
69
|
+
const payload = raw;
|
|
70
|
+
if (typeof payload.continue === 'boolean')
|
|
71
|
+
result.continue = payload.continue;
|
|
72
|
+
if (typeof payload.systemMessage === 'string')
|
|
73
|
+
result.systemMessage = payload.systemMessage;
|
|
74
|
+
if (typeof payload.suppressOutput === 'boolean')
|
|
75
|
+
result.suppressOutput = payload.suppressOutput;
|
|
76
|
+
if (typeof payload.hookSpecificOutput === 'object' && payload.hookSpecificOutput !== null) {
|
|
77
|
+
result.hookSpecificOutput = { ...payload.hookSpecificOutput };
|
|
78
|
+
}
|
|
79
|
+
if (typeof payload.decision === 'string') {
|
|
80
|
+
result.hookSpecificOutput = {
|
|
81
|
+
...(result.hookSpecificOutput ?? {}),
|
|
82
|
+
permissionDecision: payload.decision,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const eventName = result.hookSpecificOutput?.hookEventName ?? input.hookEventName ?? input.event;
|
|
87
|
+
if (eventName) {
|
|
88
|
+
result.hookSpecificOutput = {
|
|
89
|
+
hookEventName: eventName,
|
|
90
|
+
...(result.hookSpecificOutput ?? {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (!result.continue && !result.hookSpecificOutput?.permissionDecision) {
|
|
94
|
+
if (decision.permissionDecision)
|
|
95
|
+
result.hookSpecificOutput = {
|
|
96
|
+
...(result.hookSpecificOutput ?? {}),
|
|
97
|
+
permissionDecision: decision.permissionDecision,
|
|
98
|
+
};
|
|
99
|
+
else
|
|
100
|
+
result.hookSpecificOutput = { ...(result.hookSpecificOutput ?? {}), permissionDecision: 'deny' };
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
async function main() {
|
|
105
|
+
const [delegatePath, ...restArgs] = process.argv.slice(2);
|
|
106
|
+
if (!delegatePath) {
|
|
107
|
+
console.log(JSON.stringify({ continue: true }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const input = await (async () => {
|
|
111
|
+
const chunks = [];
|
|
112
|
+
let totalBytes = 0;
|
|
113
|
+
for await (const chunk of process.stdin) {
|
|
114
|
+
chunks.push(chunk);
|
|
115
|
+
totalBytes += chunk.length;
|
|
116
|
+
if (totalBytes > 10 * 1024 * 1024)
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const raw = Buffer.concat(chunks.map(c => typeof c === 'string' ? Buffer.from(c) : c)).toString('utf-8').trim();
|
|
120
|
+
if (!raw)
|
|
121
|
+
return {};
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(raw);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
try {
|
|
130
|
+
const result = spawnSync(process.execPath, [delegatePath, ...restArgs], {
|
|
131
|
+
encoding: 'utf-8',
|
|
132
|
+
input: JSON.stringify(input),
|
|
133
|
+
cwd: process.cwd(),
|
|
134
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
135
|
+
});
|
|
136
|
+
if (result.error) {
|
|
137
|
+
console.log(JSON.stringify({ continue: true }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const parsed = lastJSONObjectFromText(result.stdout ?? '');
|
|
141
|
+
if (!parsed) {
|
|
142
|
+
console.log(JSON.stringify({ continue: true }));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const output = normalizeOutput(parsed, input);
|
|
146
|
+
console.log(JSON.stringify(output));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
console.log(JSON.stringify({ continue: true }));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
main().catch(() => {
|
|
153
|
+
console.log(JSON.stringify({ continue: true }));
|
|
154
|
+
});
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - 인덱스 캐시는 isIndexStale()에 의존 (resetIndexCache 미사용)
|
|
14
14
|
* → 디렉토리 mtime이 변하지 않으면 캐시 재사용 (성능)
|
|
15
15
|
*/
|
|
16
|
-
import type { SolutionDirConfig } from '../engine/solution-index.js';
|
|
17
16
|
import type { SolutionStatus, SolutionType } from '../engine/solution-format.js';
|
|
17
|
+
import type { SolutionDirConfig } from '../engine/solution-index.js';
|
|
18
18
|
export interface SearchOptions {
|
|
19
19
|
dirs?: SolutionDirConfig[];
|
|
20
20
|
type?: SolutionType;
|
|
@@ -25,7 +25,7 @@ export interface ListOptions {
|
|
|
25
25
|
dirs?: SolutionDirConfig[];
|
|
26
26
|
status?: SolutionStatus;
|
|
27
27
|
type?: SolutionType;
|
|
28
|
-
scope?: 'me' | 'team' | 'project';
|
|
28
|
+
scope?: 'me' | 'team' | 'project' | 'universal';
|
|
29
29
|
sort?: 'confidence' | 'updated' | 'name';
|
|
30
30
|
}
|
|
31
31
|
export interface SolutionSummary {
|
|
@@ -33,7 +33,7 @@ export interface SolutionSummary {
|
|
|
33
33
|
status: SolutionStatus;
|
|
34
34
|
confidence: number;
|
|
35
35
|
type: SolutionType;
|
|
36
|
-
scope: 'me' | 'team' | 'project';
|
|
36
|
+
scope: 'me' | 'team' | 'project' | 'universal';
|
|
37
37
|
tags: string[];
|
|
38
38
|
}
|
|
39
39
|
export interface SearchResult extends SolutionSummary {
|
|
@@ -45,7 +45,7 @@ export interface SolutionDetail {
|
|
|
45
45
|
status: SolutionStatus;
|
|
46
46
|
confidence: number;
|
|
47
47
|
type: SolutionType;
|
|
48
|
-
scope: 'me' | 'team' | 'project';
|
|
48
|
+
scope: 'me' | 'team' | 'project' | 'universal';
|
|
49
49
|
tags: string[];
|
|
50
50
|
identifiers: string[];
|
|
51
51
|
context: string;
|
|
@@ -55,7 +55,7 @@ export interface SolutionStats {
|
|
|
55
55
|
total: number;
|
|
56
56
|
byStatus: Record<SolutionStatus, number>;
|
|
57
57
|
byType: Record<SolutionType, number>;
|
|
58
|
-
byScope: Record<'me' | 'team' | 'project', number>;
|
|
58
|
+
byScope: Record<'me' | 'team' | 'project' | 'universal', number>;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* 기본 솔루션 디렉토리 목록 생성.
|