@wooojin/forgen 0.2.0 → 0.3.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/CHANGELOG.md +72 -0
- package/README.ja.md +79 -14
- package/README.ko.md +100 -14
- package/README.md +124 -17
- package/README.zh.md +79 -14
- 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/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 +273 -0
- 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 +26 -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 +108 -0
- package/dist/core/dashboard.js +495 -0
- package/dist/core/doctor.js +151 -21
- package/dist/core/drift-score.d.ts +49 -0
- package/dist/core/drift-score.js +87 -0
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/mcp-config.d.ts +2 -0
- package/dist/core/mcp-config.js +6 -1
- package/dist/core/paths.d.ts +6 -1
- package/dist/core/paths.js +18 -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-export.d.ts +41 -0
- package/dist/engine/compound-export.js +169 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/compound-loop.js +18 -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-format.d.ts +2 -2
- package/dist/engine/solution-format.js +249 -34
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-matcher.d.ts +30 -1
- package/dist/engine/solution-matcher.js +235 -45
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +15 -0
- package/dist/hooks/context-guard.js +218 -56
- package/dist/hooks/db-guard.js +2 -2
- package/dist/hooks/hook-config.d.ts +27 -1
- package/dist/hooks/hook-config.js +72 -12
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/intent-classifier.d.ts +0 -2
- package/dist/hooks/intent-classifier.js +32 -18
- package/dist/hooks/keyword-detector.js +126 -204
- package/dist/hooks/notepad-injector.js +2 -2
- package/dist/hooks/permission-handler.js +2 -2
- package/dist/hooks/post-tool-failure.js +12 -6
- package/dist/hooks/post-tool-handlers.d.ts +1 -1
- package/dist/hooks/post-tool-handlers.js +14 -11
- package/dist/hooks/post-tool-use.d.ts +11 -0
- package/dist/hooks/post-tool-use.js +184 -71
- package/dist/hooks/pre-compact.d.ts +11 -1
- package/dist/hooks/pre-compact.js +112 -37
- package/dist/hooks/pre-tool-use.js +86 -56
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/session-recovery.js +256 -236
- package/dist/hooks/shared/hook-response.d.ts +4 -4
- package/dist/hooks/shared/hook-response.js +13 -24
- package/dist/hooks/shared/hook-timing.d.ts +15 -0
- package/dist/hooks/shared/hook-timing.js +64 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +47 -16
- package/dist/hooks/slop-detector.js +3 -3
- package/dist/hooks/solution-injector.js +224 -197
- package/dist/hooks/subagent-tracker.js +2 -2
- 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/renderer/rule-renderer.js +9 -11
- 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 +266 -0
- 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/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/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
package/dist/core/doctor.js
CHANGED
|
@@ -2,7 +2,8 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_PHILOSOPHY, ME_SOLUTIONS, ME_RULES, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
|
|
5
|
+
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_PHILOSOPHY, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
|
|
6
|
+
import { getTimingStats } from '../hooks/shared/hook-timing.js';
|
|
6
7
|
/** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
|
|
7
8
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
8
9
|
function check(label, condition, hint) {
|
|
@@ -104,6 +105,60 @@ export async function runDoctor() {
|
|
|
104
105
|
}
|
|
105
106
|
console.log(` Claude Code sessions: ${CLAUDE_PROJECTS_DIR}`);
|
|
106
107
|
console.log();
|
|
108
|
+
// Hook Health: recent error tracking
|
|
109
|
+
console.log(' [Hook Health]');
|
|
110
|
+
try {
|
|
111
|
+
const hookErrorsPath = path.join(STATE_DIR, 'hook-errors.jsonl');
|
|
112
|
+
if (exists(hookErrorsPath)) {
|
|
113
|
+
const content = fs.readFileSync(hookErrorsPath, 'utf-8');
|
|
114
|
+
const entries = content.trim().split('\n')
|
|
115
|
+
.map(line => { try {
|
|
116
|
+
return JSON.parse(line);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
} })
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
const byHook = new Map();
|
|
123
|
+
for (const e of entries) {
|
|
124
|
+
byHook.set(e.hook, (byHook.get(e.hook) ?? 0) + 1);
|
|
125
|
+
}
|
|
126
|
+
if (byHook.size === 0) {
|
|
127
|
+
console.log(' No hook errors recorded.');
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
for (const [hook, count] of [...byHook.entries()].sort((a, b) => b[1] - a[1])) {
|
|
131
|
+
console.log(` ${hook}: ${count} error(s)`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(' No hook errors recorded.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
console.log(' Unable to read hook error log.');
|
|
141
|
+
}
|
|
142
|
+
console.log();
|
|
143
|
+
// Hook Timing: performance stats
|
|
144
|
+
console.log(' [Hook Timing]');
|
|
145
|
+
const timingStats = getTimingStats();
|
|
146
|
+
if (timingStats.length === 0) {
|
|
147
|
+
console.log(' No timing data collected yet.');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(' Hook Count p50ms p95ms max ms');
|
|
151
|
+
console.log(' ' + '-'.repeat(56));
|
|
152
|
+
for (const s of timingStats) {
|
|
153
|
+
const hook = s.hook.padEnd(22);
|
|
154
|
+
const count = String(s.count).padStart(5);
|
|
155
|
+
const p50 = String(s.p50).padStart(7);
|
|
156
|
+
const p95 = String(s.p95).padStart(7);
|
|
157
|
+
const max = String(s.max).padStart(8);
|
|
158
|
+
console.log(` ${hook}${count}${p50}${p95}${max}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log();
|
|
107
162
|
console.log();
|
|
108
163
|
// v1: 팀 팩 시스템 제거. 개인 모드만 지원.
|
|
109
164
|
console.log(' [Pack Connections]');
|
|
@@ -149,32 +204,107 @@ export async function runDoctor() {
|
|
|
149
204
|
console.log();
|
|
150
205
|
}
|
|
151
206
|
}
|
|
152
|
-
//
|
|
153
|
-
console.log(' [
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
207
|
+
// Harness Maturity section
|
|
208
|
+
console.log(' [Harness Maturity]');
|
|
209
|
+
const cwd = process.cwd();
|
|
210
|
+
// 1. Preparation
|
|
211
|
+
const hasClaude = fs.existsSync(path.join(cwd, 'CLAUDE.md'));
|
|
212
|
+
let rulesCount = 0;
|
|
213
|
+
try {
|
|
214
|
+
const rulesDir = path.join(cwd, '.claude', 'rules');
|
|
215
|
+
if (fs.existsSync(rulesDir)) {
|
|
216
|
+
rulesCount = fs.readdirSync(rulesDir).filter(f => f.endsWith('.md')).length;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch { /* fail-open */ }
|
|
220
|
+
let hooksActive = 0;
|
|
221
|
+
try {
|
|
222
|
+
const hooksJsonPath = path.join(cwd, 'hooks', 'hooks.json');
|
|
223
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
224
|
+
const hooksData = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
|
|
225
|
+
if (hooksData.hooks && typeof hooksData.hooks === 'object') {
|
|
226
|
+
for (const eventHooks of Object.values(hooksData.hooks)) {
|
|
227
|
+
if (Array.isArray(eventHooks)) {
|
|
228
|
+
for (const group of eventHooks) {
|
|
229
|
+
if (Array.isArray(group.hooks)) {
|
|
230
|
+
hooksActive += (group.hooks).length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
167
234
|
}
|
|
168
235
|
}
|
|
169
236
|
}
|
|
170
|
-
catch {
|
|
171
|
-
console.log(' (hook-errors.json read failed)');
|
|
172
|
-
}
|
|
173
237
|
}
|
|
174
|
-
|
|
175
|
-
|
|
238
|
+
catch { /* fail-open */ }
|
|
239
|
+
const prepL = hasClaude && rulesCount >= 3 && hooksActive > 0 ? 'L3' : hasClaude && hooksActive > 0 ? 'L2' : hasClaude ? 'L1' : 'L0';
|
|
240
|
+
// 2. Context
|
|
241
|
+
let solutionsCount = 0;
|
|
242
|
+
try {
|
|
243
|
+
if (exists(ME_SOLUTIONS))
|
|
244
|
+
solutionsCount = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md')).length;
|
|
245
|
+
}
|
|
246
|
+
catch { /* fail-open */ }
|
|
247
|
+
let behaviorCount = 0;
|
|
248
|
+
try {
|
|
249
|
+
if (exists(ME_BEHAVIOR))
|
|
250
|
+
behaviorCount = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.md')).length;
|
|
176
251
|
}
|
|
252
|
+
catch { /* fail-open */ }
|
|
253
|
+
const ctxL = solutionsCount >= 5 && behaviorCount >= 3 ? 'L3' : solutionsCount >= 3 || behaviorCount >= 1 ? 'L2' : solutionsCount > 0 || behaviorCount > 0 ? 'L1' : 'L0';
|
|
254
|
+
// 3. Execution
|
|
255
|
+
const hasSkills = exists(ME_SKILLS);
|
|
256
|
+
const execL = hasSkills ? 'L2' : 'L1';
|
|
257
|
+
// 4. Validation
|
|
258
|
+
const hasTests = fs.existsSync(path.join(cwd, 'tests'));
|
|
259
|
+
const hasCI = fs.existsSync(path.join(cwd, '.github', 'workflows'));
|
|
260
|
+
const validL = hasTests && hasCI ? 'L3' : hasTests ? 'L2' : 'L1';
|
|
261
|
+
// 5. Improvement: reflection rate from solutions
|
|
262
|
+
let reflectionRate = 0;
|
|
263
|
+
try {
|
|
264
|
+
if (exists(ME_SOLUTIONS)) {
|
|
265
|
+
const solFiles = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
|
|
266
|
+
if (solFiles.length > 0) {
|
|
267
|
+
let reflected = 0;
|
|
268
|
+
for (const f of solFiles) {
|
|
269
|
+
try {
|
|
270
|
+
const content = fs.readFileSync(path.join(ME_SOLUTIONS, f), 'utf-8');
|
|
271
|
+
const match = content.match(/reflected:\s*(\d+)/);
|
|
272
|
+
if (match && parseInt(match[1], 10) > 0)
|
|
273
|
+
reflected++;
|
|
274
|
+
}
|
|
275
|
+
catch { /* skip */ }
|
|
276
|
+
}
|
|
277
|
+
reflectionRate = Math.round((reflected / solFiles.length) * 100);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { /* fail-open */ }
|
|
282
|
+
const improvL = reflectionRate > 0 ? 'L3' : solutionsCount > 0 ? 'L2' : 'L1';
|
|
283
|
+
const levelIcon = (l) => l === 'L3' ? '✓' : l === 'L2' ? '✓' : l === 'L1' ? '✗' : '✗';
|
|
284
|
+
console.log(` Axis Level Detail`);
|
|
285
|
+
console.log(` ${'─'.repeat(55)}`);
|
|
286
|
+
console.log(` ${levelIcon(prepL)} Preparation ${prepL} CLAUDE.md:${hasClaude ? 'yes' : 'no'}, rules:${rulesCount}, hooks:${hooksActive}`);
|
|
287
|
+
console.log(` ${levelIcon(ctxL)} Context ${ctxL} solutions:${solutionsCount}, behavior:${behaviorCount}`);
|
|
288
|
+
console.log(` ${levelIcon(execL)} Execution ${execL} skills:${hasSkills ? 'yes' : 'no'}`);
|
|
289
|
+
console.log(` ${levelIcon(validL)} Validation ${validL} tests:${hasTests ? 'yes' : 'no'}, CI:${hasCI ? 'yes' : 'no'}`);
|
|
290
|
+
console.log(` ${levelIcon(improvL)} Improvement ${improvL} reflection:${reflectionRate}%`);
|
|
177
291
|
console.log();
|
|
292
|
+
// Quick wins: suggest for lowest scoring axes
|
|
293
|
+
const axes = [
|
|
294
|
+
{ name: 'Preparation', level: prepL, hint: 'Add CLAUDE.md + .claude/rules/ files' },
|
|
295
|
+
{ name: 'Context', level: ctxL, hint: 'Run /compound to accumulate solutions' },
|
|
296
|
+
{ name: 'Execution', level: execL, hint: 'Promote solutions to skills' },
|
|
297
|
+
{ name: 'Validation', level: validL, hint: 'Add tests/ dir and .github/workflows' },
|
|
298
|
+
{ name: 'Improvement', level: improvL, hint: 'Reflect on existing solutions' },
|
|
299
|
+
];
|
|
300
|
+
const quickWins = axes.filter(a => a.level === 'L0' || a.level === 'L1').slice(0, 3);
|
|
301
|
+
if (quickWins.length > 0) {
|
|
302
|
+
console.log(' Quick Wins (Top 3):');
|
|
303
|
+
for (const win of quickWins) {
|
|
304
|
+
console.log(` → ${win.name}: ${win.hint}`);
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
}
|
|
178
308
|
// 현재 디렉토리 git 정보
|
|
179
309
|
console.log(' [Git]');
|
|
180
310
|
try {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Drift Score (Session Drift Detection)
|
|
3
|
+
*
|
|
4
|
+
* 세션 내 수정 패턴을 추적하여 drift(산만/반복 수정)를 감지.
|
|
5
|
+
* EWMA(Exponentially Weighted Moving Average) 기반 이동평균으로
|
|
6
|
+
* 최근 수정 강도를 측정하고, 임계값 초과 시 경고.
|
|
7
|
+
*
|
|
8
|
+
* Codex 합의: DriftState + evaluateDrift 2개만. 최소 인터페이스.
|
|
9
|
+
*/
|
|
10
|
+
/** Drift 상태 (세션 단위, STATE_DIR에 저장) */
|
|
11
|
+
export interface DriftState {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
totalEdits: number;
|
|
14
|
+
totalReverts: number;
|
|
15
|
+
/** EWMA edit rate (0~1, 높을수록 최근 수정 빈도 높음) */
|
|
16
|
+
ewmaEditRate: number;
|
|
17
|
+
/** EWMA revert rate (0~1) */
|
|
18
|
+
ewmaRevertRate: number;
|
|
19
|
+
/** 최근 경고 timestamp (쿨다운용) */
|
|
20
|
+
lastWarningAt: number;
|
|
21
|
+
lastCriticalAt: number;
|
|
22
|
+
hardCapReached: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface DriftResult {
|
|
25
|
+
level: 'normal' | 'warning' | 'critical' | 'hardcap';
|
|
26
|
+
score: number;
|
|
27
|
+
message: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface DriftThresholds {
|
|
30
|
+
alpha?: number;
|
|
31
|
+
warningEdits?: number;
|
|
32
|
+
criticalEdits?: number;
|
|
33
|
+
criticalReverts?: number;
|
|
34
|
+
hardCapEdits?: number;
|
|
35
|
+
warningCooldownMs?: number;
|
|
36
|
+
criticalCooldownMs?: number;
|
|
37
|
+
}
|
|
38
|
+
/** EWMA 업데이트 (순수 함수) */
|
|
39
|
+
export declare function updateEwma(prev: number, sample: number, alpha: number): number;
|
|
40
|
+
/** 새 DriftState 생성 */
|
|
41
|
+
export declare function createDriftState(sessionId: string): DriftState;
|
|
42
|
+
/**
|
|
43
|
+
* 도구 호출 이벤트로 drift 상태를 갱신하고 평가 결과를 반환.
|
|
44
|
+
* @param state 현재 상태 (mutate됨)
|
|
45
|
+
* @param isEdit Write/Edit 도구 호출 여부
|
|
46
|
+
* @param isRevert revert 감지 여부
|
|
47
|
+
* @param thresholds 커스텀 임계치 (hook-config에서 로드)
|
|
48
|
+
*/
|
|
49
|
+
export declare function evaluateDrift(state: DriftState, isEdit: boolean, isRevert: boolean, thresholds?: DriftThresholds): DriftResult;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Drift Score (Session Drift Detection)
|
|
3
|
+
*
|
|
4
|
+
* 세션 내 수정 패턴을 추적하여 drift(산만/반복 수정)를 감지.
|
|
5
|
+
* EWMA(Exponentially Weighted Moving Average) 기반 이동평균으로
|
|
6
|
+
* 최근 수정 강도를 측정하고, 임계값 초과 시 경고.
|
|
7
|
+
*
|
|
8
|
+
* Codex 합의: DriftState + evaluateDrift 2개만. 최소 인터페이스.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
alpha: 0.35,
|
|
12
|
+
warningEdits: 15,
|
|
13
|
+
criticalEdits: 30,
|
|
14
|
+
criticalReverts: 2,
|
|
15
|
+
hardCapEdits: 50,
|
|
16
|
+
warningCooldownMs: 5 * 60 * 1000,
|
|
17
|
+
criticalCooldownMs: 10 * 60 * 1000,
|
|
18
|
+
};
|
|
19
|
+
/** EWMA 업데이트 (순수 함수) */
|
|
20
|
+
export function updateEwma(prev, sample, alpha) {
|
|
21
|
+
return alpha * sample + (1 - alpha) * prev;
|
|
22
|
+
}
|
|
23
|
+
/** 새 DriftState 생성 */
|
|
24
|
+
export function createDriftState(sessionId) {
|
|
25
|
+
return {
|
|
26
|
+
sessionId,
|
|
27
|
+
totalEdits: 0,
|
|
28
|
+
totalReverts: 0,
|
|
29
|
+
ewmaEditRate: 0,
|
|
30
|
+
ewmaRevertRate: 0,
|
|
31
|
+
lastWarningAt: 0,
|
|
32
|
+
lastCriticalAt: 0,
|
|
33
|
+
hardCapReached: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 도구 호출 이벤트로 drift 상태를 갱신하고 평가 결과를 반환.
|
|
38
|
+
* @param state 현재 상태 (mutate됨)
|
|
39
|
+
* @param isEdit Write/Edit 도구 호출 여부
|
|
40
|
+
* @param isRevert revert 감지 여부
|
|
41
|
+
* @param thresholds 커스텀 임계치 (hook-config에서 로드)
|
|
42
|
+
*/
|
|
43
|
+
export function evaluateDrift(state, isEdit, isRevert, thresholds = {}) {
|
|
44
|
+
const t = { ...DEFAULTS, ...thresholds };
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
// Update counters
|
|
47
|
+
if (isEdit)
|
|
48
|
+
state.totalEdits++;
|
|
49
|
+
if (isRevert)
|
|
50
|
+
state.totalReverts++;
|
|
51
|
+
// Update EWMA
|
|
52
|
+
state.ewmaEditRate = updateEwma(state.ewmaEditRate, isEdit ? 1 : 0, t.alpha);
|
|
53
|
+
state.ewmaRevertRate = updateEwma(state.ewmaRevertRate, isRevert ? 1 : 0, t.alpha);
|
|
54
|
+
// Calculate drift score: edit rate 65% + revert rate 35%
|
|
55
|
+
const rawScore = (state.ewmaEditRate * 65) + (state.ewmaRevertRate * 35);
|
|
56
|
+
const score = Math.min(100, Math.max(0, Math.round(rawScore)));
|
|
57
|
+
// Hard cap
|
|
58
|
+
if (state.totalEdits >= t.hardCapEdits) {
|
|
59
|
+
state.hardCapReached = true;
|
|
60
|
+
return {
|
|
61
|
+
level: 'hardcap',
|
|
62
|
+
score: 100,
|
|
63
|
+
message: `[Forgen] ⛔ Session drift hard cap reached (${state.totalEdits} edits). Stop and reassess the approach before continuing.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Critical: 2+ reverts OR 30+ edits OR score >= 78
|
|
67
|
+
if ((state.totalReverts >= t.criticalReverts || state.totalEdits >= t.criticalEdits || score >= 78) &&
|
|
68
|
+
(now - state.lastCriticalAt > t.criticalCooldownMs)) {
|
|
69
|
+
state.lastCriticalAt = now;
|
|
70
|
+
return {
|
|
71
|
+
level: 'critical',
|
|
72
|
+
score,
|
|
73
|
+
message: `[Forgen] ⚠ High drift detected (score: ${score}, edits: ${state.totalEdits}, reverts: ${state.totalReverts}). Consider stopping to redesign the approach.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Warning: 15+ edits OR score >= 52
|
|
77
|
+
if ((state.totalEdits >= t.warningEdits || score >= 52) &&
|
|
78
|
+
(now - state.lastWarningAt > t.warningCooldownMs)) {
|
|
79
|
+
state.lastWarningAt = now;
|
|
80
|
+
return {
|
|
81
|
+
level: 'warning',
|
|
82
|
+
score,
|
|
83
|
+
message: `[Forgen] Drift building up (score: ${score}, edits: ${state.totalEdits}). Review your approach if changes feel repetitive.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { level: 'normal', score, message: null };
|
|
87
|
+
}
|
package/dist/core/harness.d.ts
CHANGED
|
@@ -11,14 +11,19 @@
|
|
|
11
11
|
* - Lines 400-550: Rule file injection, gitignore, compound memory
|
|
12
12
|
* - Lines 550+: prepareHarness — main orchestration
|
|
13
13
|
*/
|
|
14
|
+
import { type RuntimeHost } from './types.js';
|
|
14
15
|
import { rollbackSettings } from './settings-lock.js';
|
|
15
16
|
import { type V1BootstrapResult } from './v1-bootstrap.js';
|
|
16
17
|
export interface V1HarnessContext {
|
|
17
18
|
cwd: string;
|
|
18
19
|
inTmux: boolean;
|
|
19
20
|
v1: V1BootstrapResult;
|
|
21
|
+
runtime: RuntimeHost;
|
|
20
22
|
}
|
|
21
23
|
/** 최초 실행 여부: ~/.forgen/ 디렉토리가 없으면 true */
|
|
22
24
|
export declare function isFirstRun(): boolean;
|
|
23
25
|
export { rollbackSettings };
|
|
24
|
-
|
|
26
|
+
interface PrepareHarnessOptions {
|
|
27
|
+
runtime?: RuntimeHost;
|
|
28
|
+
}
|
|
29
|
+
export declare function prepareHarness(cwd: string, options?: PrepareHarnessOptions): Promise<V1HarnessContext>;
|
package/dist/core/harness.js
CHANGED
|
@@ -20,6 +20,7 @@ import { buildEnv, generateClaudeRuleFiles, registerTmuxBindings } from './confi
|
|
|
20
20
|
import { createLogger } from './logger.js';
|
|
21
21
|
import { HANDOFFS_DIR, ME_BEHAVIOR, ME_DIR, ME_RULES, ME_SKILLS, ME_SOLUTIONS, SESSIONS_DIR, STATE_DIR, FORGEN_HOME } from './paths.js';
|
|
22
22
|
import { RULE_FILE_CAPS } from '../hooks/shared/injection-caps.js';
|
|
23
|
+
import { generateHooksJson } from '../hooks/hooks-generator.js';
|
|
23
24
|
import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
|
|
24
25
|
import { ConfigError } from './errors.js';
|
|
25
26
|
import { bootstrapV1Session, ensureV1Directories } from './v1-bootstrap.js';
|
|
@@ -104,7 +105,7 @@ function isForgenHookEntry(entry, pkgRoot) {
|
|
|
104
105
|
return Array.isArray(hooks) && hooks.some(h => typeof h.command === 'string' && matchesPath(h.command));
|
|
105
106
|
}
|
|
106
107
|
/** Strip existing forgen hooks from settings, merge fresh hooks.json. */
|
|
107
|
-
function mergeHooksIntoSettings(settings) {
|
|
108
|
+
function mergeHooksIntoSettings(settings, runtime, cwd) {
|
|
108
109
|
const pkgRoot = getPackageRoot();
|
|
109
110
|
const hooksConfig = settings.hooks ?? {};
|
|
110
111
|
// Remove existing forgen hooks (clean slate before re-inject)
|
|
@@ -117,18 +118,28 @@ function mergeHooksIntoSettings(settings) {
|
|
|
117
118
|
else
|
|
118
119
|
hooksConfig[event] = filtered;
|
|
119
120
|
}
|
|
120
|
-
// Read hooks.json and inject, replacing ${CLAUDE_PLUGIN_ROOT}
|
|
121
|
-
const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
|
|
122
121
|
try {
|
|
123
|
-
if (
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
if (runtime === 'codex') {
|
|
123
|
+
const generated = generateHooksJson({ cwd, runtime, pluginRoot: path.join(pkgRoot, 'dist') });
|
|
124
|
+
for (const [event, handlers] of Object.entries(generated.hooks)) {
|
|
125
|
+
if (!hooksConfig[event])
|
|
126
|
+
hooksConfig[event] = [];
|
|
127
|
+
hooksConfig[event].push(...handlers);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Read hooks.json and inject, replacing ${CLAUDE_PLUGIN_ROOT}
|
|
132
|
+
const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
|
|
133
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
134
|
+
const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
|
|
135
|
+
const hooksData = hooksJson.hooks;
|
|
136
|
+
if (hooksData) {
|
|
137
|
+
const resolved = JSON.parse(JSON.stringify(hooksData).replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pkgRoot));
|
|
138
|
+
for (const [event, handlers] of Object.entries(resolved)) {
|
|
139
|
+
if (!hooksConfig[event])
|
|
140
|
+
hooksConfig[event] = [];
|
|
141
|
+
hooksConfig[event].push(...handlers);
|
|
142
|
+
}
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
}
|
|
@@ -177,14 +188,14 @@ function applyTrustPolicyPermissions(settings, v1Result) {
|
|
|
177
188
|
* atomic write). Each phase is now a named function with a single
|
|
178
189
|
* responsibility, testable in isolation if needed.
|
|
179
190
|
*/
|
|
180
|
-
function injectSettings(env, v1Result) {
|
|
191
|
+
function injectSettings(env, v1Result, runtime, cwd) {
|
|
181
192
|
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
182
193
|
acquireLock();
|
|
183
194
|
const settings = readSettingsWithBackup();
|
|
184
195
|
// Merge env vars
|
|
185
196
|
settings.env = { ...(settings.env ?? {}), ...env };
|
|
186
197
|
applyStatusLine(settings);
|
|
187
|
-
mergeHooksIntoSettings(settings);
|
|
198
|
+
mergeHooksIntoSettings(settings, runtime, cwd);
|
|
188
199
|
applyTrustPolicyPermissions(settings, v1Result);
|
|
189
200
|
try {
|
|
190
201
|
atomicWriteFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
@@ -252,14 +263,58 @@ function installAgentsFromDir(sourceDir, targetDir, prefix, hashes) {
|
|
|
252
263
|
hashes[dstName] = newHash;
|
|
253
264
|
}
|
|
254
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* 현재 source에 없는 stale ch-*.md 에이전트 파일을 정리.
|
|
268
|
+
* forgen-managed 마커가 있는 파일만 삭제 (사용자 수정 파일 보호).
|
|
269
|
+
*/
|
|
270
|
+
function cleanupStaleAgents(sourceDir, targetDir, prefix, hashes) {
|
|
271
|
+
if (!fs.existsSync(targetDir))
|
|
272
|
+
return;
|
|
273
|
+
if (!fs.existsSync(sourceDir))
|
|
274
|
+
return;
|
|
275
|
+
// 현재 source의 유효한 파일 목록
|
|
276
|
+
const validFiles = new Set(fs.readdirSync(sourceDir)
|
|
277
|
+
.filter((f) => f.endsWith('.md'))
|
|
278
|
+
.map((f) => `${prefix}${f}`));
|
|
279
|
+
// targetDir에서 prefix로 시작하지만 유효 목록에 없는 파일 삭제
|
|
280
|
+
for (const existing of fs.readdirSync(targetDir)) {
|
|
281
|
+
if (!existing.startsWith(prefix) || !existing.endsWith('.md'))
|
|
282
|
+
continue;
|
|
283
|
+
if (validFiles.has(existing))
|
|
284
|
+
continue;
|
|
285
|
+
const filePath = path.join(targetDir, existing);
|
|
286
|
+
try {
|
|
287
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
288
|
+
// 사용자 수정 보호: forgen-managed 마커가 있고 hash가 기록된 경우만 삭제
|
|
289
|
+
const recordedHash = hashes[existing];
|
|
290
|
+
const hasMarker = content.includes('<!-- forgen-managed -->');
|
|
291
|
+
if (!hasMarker) {
|
|
292
|
+
log.debug(`에이전트 삭제 스킵: ${existing} (forgen-managed 마커 없음)`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (recordedHash && contentHash(content) !== recordedHash) {
|
|
296
|
+
log.debug(`에이전트 삭제 스킵: ${existing} (사용자 수정 감지)`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
fs.unlinkSync(filePath);
|
|
300
|
+
delete hashes[existing];
|
|
301
|
+
log.debug(`stale 에이전트 삭제: ${existing}`);
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
log.debug(`에이전트 삭제 실패: ${existing}`, e);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
255
308
|
/** 에이전트 정의 파일 설치 (패키지 내장만) */
|
|
256
309
|
function installAgents(cwd) {
|
|
257
310
|
const pkgRoot = getPackageRoot();
|
|
258
311
|
const targetDir = path.join(cwd, '.claude', 'agents');
|
|
259
312
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
260
313
|
const hashes = loadAgentHashes();
|
|
314
|
+
const sourceDir = path.join(pkgRoot, 'agents');
|
|
261
315
|
try {
|
|
262
|
-
installAgentsFromDir(
|
|
316
|
+
installAgentsFromDir(sourceDir, targetDir, 'ch-', hashes);
|
|
317
|
+
cleanupStaleAgents(sourceDir, targetDir, 'ch-', hashes);
|
|
263
318
|
saveAgentHashes(hashes);
|
|
264
319
|
}
|
|
265
320
|
catch (e) {
|
|
@@ -560,7 +615,8 @@ function checkCompoundStaleness() {
|
|
|
560
615
|
log.debug('Staleness check failed (non-fatal)', e);
|
|
561
616
|
}
|
|
562
617
|
}
|
|
563
|
-
export async function prepareHarness(cwd) {
|
|
618
|
+
export async function prepareHarness(cwd, options = {}) {
|
|
619
|
+
const runtime = options.runtime ?? 'claude';
|
|
564
620
|
try {
|
|
565
621
|
// 0. 스토리지 마이그레이션 (v5.1: ~/.compound/ → ~/.forgen/)
|
|
566
622
|
migrateToForgen();
|
|
@@ -591,8 +647,8 @@ export async function prepareHarness(cwd) {
|
|
|
591
647
|
// 3. 환경 확인
|
|
592
648
|
const inTmux = !!process.env.TMUX;
|
|
593
649
|
// 4. Claude Code 설정 주입 (환경변수 + trust 기반 permissions)
|
|
594
|
-
const env = buildEnv(cwd, v1Result.session?.session_id);
|
|
595
|
-
injectSettings(env, v1Result);
|
|
650
|
+
const env = buildEnv(cwd, v1Result.session?.session_id, runtime);
|
|
651
|
+
injectSettings(env, v1Result, runtime, cwd);
|
|
596
652
|
// 5. 에이전트 설치
|
|
597
653
|
installAgents(cwd);
|
|
598
654
|
// 6. 규칙 파일 생성 및 주입 (v1 부트스트랩 결과의 renderedRules를 직접 전달)
|
|
@@ -612,7 +668,7 @@ export async function prepareHarness(cwd) {
|
|
|
612
668
|
await startLegacySessionLog(cwd, inTmux, v1Result);
|
|
613
669
|
// 12. Compound staleness guard
|
|
614
670
|
checkCompoundStaleness();
|
|
615
|
-
return { cwd, inTmux, v1: v1Result };
|
|
671
|
+
return { cwd, inTmux, v1: v1Result, runtime };
|
|
616
672
|
}
|
|
617
673
|
catch (err) {
|
|
618
674
|
rollbackSettings();
|
package/dist/core/mcp-config.js
CHANGED
|
@@ -101,7 +101,12 @@ export async function handleMcp(args) {
|
|
|
101
101
|
for (const name of names) {
|
|
102
102
|
const cfg = installed[name];
|
|
103
103
|
console.log(` ${name}`);
|
|
104
|
-
|
|
104
|
+
if (cfg.command) {
|
|
105
|
+
console.log(` command: ${cfg.command} ${(cfg.args ?? []).join(' ')}`);
|
|
106
|
+
}
|
|
107
|
+
else if (cfg.url) {
|
|
108
|
+
console.log(` url: ${cfg.url}`);
|
|
109
|
+
}
|
|
105
110
|
if (cfg.env && Object.keys(cfg.env).length > 0) {
|
|
106
111
|
console.log(` env: ${JSON.stringify(cfg.env)}`);
|
|
107
112
|
}
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** ~/.claude/ — Claude Code 설정 디렉토리 */
|
|
2
2
|
export declare const CLAUDE_DIR: string;
|
|
3
|
+
export declare const CODEX_DIR: string;
|
|
3
4
|
/** ~/.claude/settings.json — Claude Code 설정 파일 */
|
|
4
5
|
export declare const SETTINGS_PATH: string;
|
|
5
6
|
/**
|
|
@@ -47,6 +48,10 @@ export declare const MATCH_EVAL_LOG_PATH: string;
|
|
|
47
48
|
export declare const SESSIONS_DIR: string;
|
|
48
49
|
/** ~/.forgen/config.json — 글로벌 설정 */
|
|
49
50
|
export declare const GLOBAL_CONFIG: string;
|
|
51
|
+
/** ~/.forgen/state/session-quality/ — 세션 품질 점수 */
|
|
52
|
+
export declare const SESSION_QUALITY_DIR: string;
|
|
53
|
+
/** ~/.forgen/state/meta-learning/ — 메타학습 상태 파일 */
|
|
54
|
+
export declare const META_LEARNING_DIR: string;
|
|
50
55
|
/** ~/.forgen/lab/ — Lab 적응형 최적화 엔진 데이터 */
|
|
51
56
|
export declare const LAB_DIR: string;
|
|
52
57
|
/** ~/.forgen/lab/events.jsonl — Lab 이벤트 로그 (JSONL) */
|
|
@@ -74,7 +79,7 @@ export declare const V1_RAW_LOGS_DIR: string;
|
|
|
74
79
|
/** @deprecated use GLOBAL_CONFIG */
|
|
75
80
|
export declare const V1_GLOBAL_CONFIG: string;
|
|
76
81
|
/** 모든 실행 모드 이름 (cancel/recovery 시 사용) */
|
|
77
|
-
export declare const ALL_MODES: readonly ["ralph", "autopilot", "ultrawork", "team", "pipeline", "ccg", "ralplan", "deep-interview", "
|
|
82
|
+
export declare const ALL_MODES: readonly ["ralph", "autopilot", "ultrawork", "team", "pipeline", "ccg", "ralplan", "deep-interview", "forge-loop", "ship", "retro", "learn", "calibrate"];
|
|
78
83
|
/** {repo}/.compound/ — 프로젝트 로컬 디렉토리 */
|
|
79
84
|
export declare function projectDir(cwd: string): string;
|
|
80
85
|
/** {repo}/.compound/pack.link — 팀 팩 연결 파일 */
|
package/dist/core/paths.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
const HOME = os.homedir();
|
|
4
4
|
/** ~/.claude/ — Claude Code 설정 디렉토리 */
|
|
5
5
|
export const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
6
|
+
export const CODEX_DIR = path.join(HOME, '.codex');
|
|
6
7
|
/** ~/.claude/settings.json — Claude Code 설정 파일 */
|
|
7
8
|
export const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
8
9
|
/**
|
|
@@ -50,6 +51,10 @@ export const MATCH_EVAL_LOG_PATH = path.join(STATE_DIR, 'match-eval-log.jsonl');
|
|
|
50
51
|
export const SESSIONS_DIR = path.join(FORGEN_HOME, 'sessions');
|
|
51
52
|
/** ~/.forgen/config.json — 글로벌 설정 */
|
|
52
53
|
export const GLOBAL_CONFIG = path.join(FORGEN_HOME, 'config.json');
|
|
54
|
+
/** ~/.forgen/state/session-quality/ — 세션 품질 점수 */
|
|
55
|
+
export const SESSION_QUALITY_DIR = path.join(STATE_DIR, 'session-quality');
|
|
56
|
+
/** ~/.forgen/state/meta-learning/ — 메타학습 상태 파일 */
|
|
57
|
+
export const META_LEARNING_DIR = path.join(STATE_DIR, 'meta-learning');
|
|
53
58
|
/** ~/.forgen/lab/ — Lab 적응형 최적화 엔진 데이터 */
|
|
54
59
|
export const LAB_DIR = path.join(FORGEN_HOME, 'lab');
|
|
55
60
|
/** ~/.forgen/lab/events.jsonl — Lab 이벤트 로그 (JSONL) */
|
|
@@ -80,8 +85,19 @@ export const V1_GLOBAL_CONFIG = GLOBAL_CONFIG;
|
|
|
80
85
|
// ── 레거시 ──
|
|
81
86
|
/** 모든 실행 모드 이름 (cancel/recovery 시 사용) */
|
|
82
87
|
export const ALL_MODES = [
|
|
83
|
-
'ralph',
|
|
84
|
-
'
|
|
88
|
+
'ralph',
|
|
89
|
+
'autopilot',
|
|
90
|
+
'ultrawork',
|
|
91
|
+
'team',
|
|
92
|
+
'pipeline',
|
|
93
|
+
'ccg',
|
|
94
|
+
'ralplan',
|
|
95
|
+
'deep-interview',
|
|
96
|
+
'forge-loop',
|
|
97
|
+
'ship',
|
|
98
|
+
'retro',
|
|
99
|
+
'learn',
|
|
100
|
+
'calibrate',
|
|
85
101
|
];
|
|
86
102
|
/** {repo}/.compound/ — 프로젝트 로컬 디렉토리 */
|
|
87
103
|
export function projectDir(cwd) {
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { V1HarnessContext } from './harness.js';
|
|
2
|
+
import { type RuntimeHost } from './types.js';
|
|
2
3
|
/** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
|
|
3
|
-
export declare function spawnClaude(args: string[], context: V1HarnessContext): Promise<number>;
|
|
4
|
+
export declare function spawnClaude(args: string[], context: V1HarnessContext, runtime?: RuntimeHost): Promise<number>;
|
|
4
5
|
/**
|
|
5
6
|
* 토큰 한도 도달 시 자동 재시작을 지원하는 claude 실행 래퍼.
|
|
6
7
|
* context-guard가 pending-resume.json 마커를 생성하면 쿨다운 후 재시작.
|
|
7
8
|
*/
|
|
8
|
-
export declare function spawnClaudeWithResume(args: string[], context: V1HarnessContext, contextFactory: () => Promise<V1HarnessContext
|
|
9
|
+
export declare function spawnClaudeWithResume(args: string[], context: V1HarnessContext, contextFactory: () => Promise<V1HarnessContext>, runtime?: RuntimeHost): Promise<void>;
|