@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,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Core Harness (prepareHarness entry point)
|
|
3
|
+
*
|
|
4
|
+
* v1 설계: v1-bootstrap 기반 세션 오케스트레이션.
|
|
5
|
+
* philosophy/scope/pack 의존 제거. Profile + Preset Manager + Rule Renderer.
|
|
6
|
+
*
|
|
7
|
+
* Module Structure:
|
|
8
|
+
* - Lines 1-70: Imports, utility helpers
|
|
9
|
+
* - Lines 70-220: injectSettings — Claude Code settings.json injection
|
|
10
|
+
* - Lines 220-400: Agent/skill installation helpers
|
|
11
|
+
* - Lines 400-550: Rule file injection, gitignore, compound memory
|
|
12
|
+
* - Lines 550+: prepareHarness — main orchestration
|
|
13
|
+
*/
|
|
14
|
+
import * as crypto from 'node:crypto';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as os from 'node:os';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { buildEnv, generateClaudeRuleFiles, registerTmuxBindings } from './config-injector.js';
|
|
20
|
+
import { createLogger } from './logger.js';
|
|
21
|
+
import { HANDOFFS_DIR, ME_BEHAVIOR, ME_DIR, ME_RULES, ME_SKILLS, ME_SOLUTIONS, SESSIONS_DIR, STATE_DIR, FORGEN_HOME } from './paths.js';
|
|
22
|
+
import { RULE_FILE_CAPS } from '../hooks/shared/injection-caps.js';
|
|
23
|
+
import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
|
|
24
|
+
import { ConfigError } from './errors.js';
|
|
25
|
+
import { bootstrapV1Session, ensureV1Directories } from './v1-bootstrap.js';
|
|
26
|
+
const log = createLogger('harness');
|
|
27
|
+
/** forgen 패키지 루트 */
|
|
28
|
+
function getPackageRoot() {
|
|
29
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
30
|
+
}
|
|
31
|
+
/** 최초 실행 여부: ~/.forgen/ 디렉토리가 없으면 true */
|
|
32
|
+
export function isFirstRun() {
|
|
33
|
+
return !fs.existsSync(FORGEN_HOME);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A5: all directories under FORGEN_HOME only. Pre-A5 this function
|
|
37
|
+
* also created directories under `~/.compound/` (COMPOUND_HOME), which
|
|
38
|
+
* caused a dual-reality when the migration symlink was broken. Now the
|
|
39
|
+
* migration function handles reading from the old location if needed,
|
|
40
|
+
* but ALL writes go under `~/.forgen/`.
|
|
41
|
+
*/
|
|
42
|
+
function ensureDirectories() {
|
|
43
|
+
const dirs = [
|
|
44
|
+
FORGEN_HOME,
|
|
45
|
+
ME_DIR,
|
|
46
|
+
ME_SOLUTIONS,
|
|
47
|
+
ME_BEHAVIOR,
|
|
48
|
+
ME_RULES,
|
|
49
|
+
ME_SKILLS,
|
|
50
|
+
SESSIONS_DIR,
|
|
51
|
+
STATE_DIR,
|
|
52
|
+
HANDOFFS_DIR,
|
|
53
|
+
path.join(FORGEN_HOME, 'plans'),
|
|
54
|
+
path.join(FORGEN_HOME, 'specs'),
|
|
55
|
+
path.join(FORGEN_HOME, 'artifacts', 'ask'),
|
|
56
|
+
];
|
|
57
|
+
for (const dir of dirs) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
ensureV1Directories();
|
|
61
|
+
}
|
|
62
|
+
export { rollbackSettings };
|
|
63
|
+
// ── Settings Injection ──
|
|
64
|
+
const FORGEN_PERMISSION_RULES = new Set([
|
|
65
|
+
'# forgen-managed',
|
|
66
|
+
'Bash(rm -rf *)',
|
|
67
|
+
'Bash(git push --force*)',
|
|
68
|
+
'Bash(git reset --hard*)',
|
|
69
|
+
]);
|
|
70
|
+
function stripForgenManagedRules(rules) {
|
|
71
|
+
return rules.filter(r => !FORGEN_PERMISSION_RULES.has(r));
|
|
72
|
+
}
|
|
73
|
+
/** Claude Code settings.json에 하네스 환경변수 + 훅 주입 */
|
|
74
|
+
// ── B9: injectSettings sub-phases (extracted from 128-line monolith) ──
|
|
75
|
+
/** Read settings.json with backup, or return empty object on failure. */
|
|
76
|
+
function readSettingsWithBackup() {
|
|
77
|
+
if (!fs.existsSync(SETTINGS_PATH))
|
|
78
|
+
return {};
|
|
79
|
+
try {
|
|
80
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
81
|
+
fs.copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH);
|
|
82
|
+
return settings;
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
log.debug('settings.json 파싱 실패, 빈 설정으로 시작', new ConfigError('settings.json parse failed', { configPath: SETTINGS_PATH, cause: e }));
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Apply forgen statusLine only if user hasn't set a custom one. */
|
|
90
|
+
function applyStatusLine(settings) {
|
|
91
|
+
const existing = settings.statusLine;
|
|
92
|
+
const isForgenOwned = !existing || !existing.command || existing.command.startsWith('forgen');
|
|
93
|
+
if (isForgenOwned) {
|
|
94
|
+
settings.statusLine = { type: 'command', command: 'forgen me' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Check if a settings.json hook entry was installed by forgen. */
|
|
98
|
+
function isForgenHookEntry(entry, pkgRoot) {
|
|
99
|
+
const distHooksPath = path.join(pkgRoot, 'dist', 'hooks');
|
|
100
|
+
const matchesPath = (cmd) => cmd.includes(distHooksPath) || /[\\/]dist[\\/]hooks[\\/].*\.js/.test(cmd);
|
|
101
|
+
if (typeof entry.command === 'string' && matchesPath(entry.command))
|
|
102
|
+
return true;
|
|
103
|
+
const hooks = entry.hooks;
|
|
104
|
+
return Array.isArray(hooks) && hooks.some(h => typeof h.command === 'string' && matchesPath(h.command));
|
|
105
|
+
}
|
|
106
|
+
/** Strip existing forgen hooks from settings, merge fresh hooks.json. */
|
|
107
|
+
function mergeHooksIntoSettings(settings) {
|
|
108
|
+
const pkgRoot = getPackageRoot();
|
|
109
|
+
const hooksConfig = settings.hooks ?? {};
|
|
110
|
+
// Remove existing forgen hooks (clean slate before re-inject)
|
|
111
|
+
for (const [event, entries] of Object.entries(hooksConfig)) {
|
|
112
|
+
if (!Array.isArray(entries))
|
|
113
|
+
continue;
|
|
114
|
+
const filtered = entries.filter(h => !isForgenHookEntry(h, pkgRoot));
|
|
115
|
+
if (filtered.length === 0)
|
|
116
|
+
delete hooksConfig[event];
|
|
117
|
+
else
|
|
118
|
+
hooksConfig[event] = filtered;
|
|
119
|
+
}
|
|
120
|
+
// Read hooks.json and inject, replacing ${CLAUDE_PLUGIN_ROOT}
|
|
121
|
+
const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
|
|
122
|
+
try {
|
|
123
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
124
|
+
const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
|
|
125
|
+
const hooksData = hooksJson.hooks;
|
|
126
|
+
if (hooksData) {
|
|
127
|
+
const resolved = JSON.parse(JSON.stringify(hooksData).replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pkgRoot));
|
|
128
|
+
for (const [event, handlers] of Object.entries(resolved)) {
|
|
129
|
+
if (!hooksConfig[event])
|
|
130
|
+
hooksConfig[event] = [];
|
|
131
|
+
hooksConfig[event].push(...handlers);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
log.debug('hooks.json 로드 실패', e);
|
|
138
|
+
}
|
|
139
|
+
settings.hooks = Object.keys(hooksConfig).length > 0 ? hooksConfig : undefined;
|
|
140
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
141
|
+
delete settings.hooks;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Apply v1 trust policy → permissions (deny/ask lists). */
|
|
145
|
+
function applyTrustPolicyPermissions(settings, v1Result) {
|
|
146
|
+
if (!v1Result.session)
|
|
147
|
+
return;
|
|
148
|
+
const trust = v1Result.session.effective_trust_policy;
|
|
149
|
+
const permissions = settings.permissions ?? {};
|
|
150
|
+
const existingDeny = stripForgenManagedRules(permissions.deny ?? []);
|
|
151
|
+
if (trust === '가드레일 우선') {
|
|
152
|
+
permissions.deny = [
|
|
153
|
+
...existingDeny, '# forgen-managed',
|
|
154
|
+
'Bash(rm -rf *)', 'Bash(git push --force*)', 'Bash(git reset --hard*)',
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
else if (trust === '승인 완화') {
|
|
158
|
+
const existingAsk = stripForgenManagedRules(permissions.ask ?? []);
|
|
159
|
+
permissions.ask = [
|
|
160
|
+
...existingAsk, '# forgen-managed',
|
|
161
|
+
'Bash(rm -rf *)', 'Bash(git push --force*)',
|
|
162
|
+
];
|
|
163
|
+
permissions.deny = existingDeny.length > 0 ? existingDeny : undefined;
|
|
164
|
+
}
|
|
165
|
+
// '완전 신뢰 실행': 추가 제한 없음
|
|
166
|
+
if (!permissions.deny?.length)
|
|
167
|
+
delete permissions.deny;
|
|
168
|
+
if (!permissions.ask?.length)
|
|
169
|
+
delete permissions.ask;
|
|
170
|
+
if (Object.keys(permissions).length > 0)
|
|
171
|
+
settings.permissions = permissions;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* B9: injectSettings — now a ~20-line coordinator calling the extracted
|
|
175
|
+
* sub-phases above. Pre-B9 this was 128 lines with interleaved phases
|
|
176
|
+
* (read/backup, env merge, statusLine, hook strip+inject, trust policy,
|
|
177
|
+
* atomic write). Each phase is now a named function with a single
|
|
178
|
+
* responsibility, testable in isolation if needed.
|
|
179
|
+
*/
|
|
180
|
+
function injectSettings(env, v1Result) {
|
|
181
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
182
|
+
acquireLock();
|
|
183
|
+
const settings = readSettingsWithBackup();
|
|
184
|
+
// Merge env vars
|
|
185
|
+
settings.env = { ...(settings.env ?? {}), ...env };
|
|
186
|
+
applyStatusLine(settings);
|
|
187
|
+
mergeHooksIntoSettings(settings);
|
|
188
|
+
applyTrustPolicyPermissions(settings, v1Result);
|
|
189
|
+
try {
|
|
190
|
+
atomicWriteFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
rollbackSettings();
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
releaseLock();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ── Agent Installation ──
|
|
201
|
+
function contentHash(content) {
|
|
202
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
|
|
203
|
+
}
|
|
204
|
+
const AGENT_HASHES_PATH = path.join(STATE_DIR, 'agent-hashes.json');
|
|
205
|
+
function loadAgentHashes() {
|
|
206
|
+
try {
|
|
207
|
+
if (fs.existsSync(AGENT_HASHES_PATH)) {
|
|
208
|
+
return JSON.parse(fs.readFileSync(AGENT_HASHES_PATH, 'utf-8'));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
log.debug('에이전트 해시 맵 로드 실패', e);
|
|
213
|
+
}
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
function saveAgentHashes(hashes) {
|
|
217
|
+
try {
|
|
218
|
+
fs.mkdirSync(path.dirname(AGENT_HASHES_PATH), { recursive: true });
|
|
219
|
+
fs.writeFileSync(AGENT_HASHES_PATH, JSON.stringify(hashes, null, 2));
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
log.debug('에이전트 해시 맵 저장 실패', e);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function installAgentsFromDir(sourceDir, targetDir, prefix, hashes) {
|
|
226
|
+
if (!fs.existsSync(sourceDir))
|
|
227
|
+
return;
|
|
228
|
+
const files = fs.readdirSync(sourceDir).filter((f) => f.endsWith('.md'));
|
|
229
|
+
for (const file of files) {
|
|
230
|
+
const src = path.join(sourceDir, file);
|
|
231
|
+
const dstName = `${prefix}${file}`;
|
|
232
|
+
const dst = path.join(targetDir, dstName);
|
|
233
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
234
|
+
const newHash = contentHash(content);
|
|
235
|
+
if (fs.existsSync(dst)) {
|
|
236
|
+
const existing = fs.readFileSync(dst, 'utf-8');
|
|
237
|
+
if (existing === content) {
|
|
238
|
+
hashes[dstName] = newHash;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const recordedHash = hashes[dstName];
|
|
242
|
+
if (recordedHash && contentHash(existing) !== recordedHash) {
|
|
243
|
+
log.debug(`에이전트 파일 보호: ${dstName} (사용자 수정 감지)`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!recordedHash && !existing.includes('<!-- forgen-managed -->')) {
|
|
247
|
+
log.debug(`에이전트 파일 보호: ${dstName} (레거시 사용자 수정 감지)`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
fs.writeFileSync(dst, content);
|
|
252
|
+
hashes[dstName] = newHash;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** 에이전트 정의 파일 설치 (패키지 내장만) */
|
|
256
|
+
function installAgents(cwd) {
|
|
257
|
+
const pkgRoot = getPackageRoot();
|
|
258
|
+
const targetDir = path.join(cwd, '.claude', 'agents');
|
|
259
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
260
|
+
const hashes = loadAgentHashes();
|
|
261
|
+
try {
|
|
262
|
+
installAgentsFromDir(path.join(pkgRoot, 'agents'), targetDir, 'ch-', hashes);
|
|
263
|
+
saveAgentHashes(hashes);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
log.debug('에이전트 설치 실패', e);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ── Slash Commands ──
|
|
270
|
+
function buildCommandContent(skillContent, skillName) {
|
|
271
|
+
const descMatch = skillContent.match(/description:\s*(.+)/);
|
|
272
|
+
const desc = descMatch?.[1]?.trim() ?? skillName;
|
|
273
|
+
return `# ${desc}\n\n<!-- forgen-managed -->\n\nActivate Forgen "${skillName}" mode for the task: $ARGUMENTS\n\n${skillContent}`;
|
|
274
|
+
}
|
|
275
|
+
function safeWriteCommand(cmdPath, content) {
|
|
276
|
+
if (fs.existsSync(cmdPath)) {
|
|
277
|
+
const existing = fs.readFileSync(cmdPath, 'utf-8');
|
|
278
|
+
if (!existing.includes('<!-- forgen-managed -->'))
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
fs.writeFileSync(cmdPath, content);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
function cleanupStaleCommands(commandsDir, validFiles) {
|
|
285
|
+
if (!fs.existsSync(commandsDir))
|
|
286
|
+
return 0;
|
|
287
|
+
let removed = 0;
|
|
288
|
+
for (const file of fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'))) {
|
|
289
|
+
if (validFiles.has(file))
|
|
290
|
+
continue;
|
|
291
|
+
const filePath = path.join(commandsDir, file);
|
|
292
|
+
try {
|
|
293
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
294
|
+
if (content.includes('<!-- forgen-managed -->')) {
|
|
295
|
+
fs.unlinkSync(filePath);
|
|
296
|
+
removed++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
log.debug(`stale 명령 파일 정리 실패: ${file}`, e);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return removed;
|
|
304
|
+
}
|
|
305
|
+
/** 스킬을 Claude Code 슬래시 명령으로 설치 (패키지 내장만) */
|
|
306
|
+
function installSlashCommands(_cwd) {
|
|
307
|
+
const pkgRoot = getPackageRoot();
|
|
308
|
+
let skillsDir = path.join(pkgRoot, 'commands');
|
|
309
|
+
if (!fs.existsSync(skillsDir)) {
|
|
310
|
+
skillsDir = path.join(pkgRoot, 'skills');
|
|
311
|
+
}
|
|
312
|
+
const homeDir = os.homedir();
|
|
313
|
+
const globalCommandsDir = path.join(homeDir, '.claude', 'commands', 'forgen');
|
|
314
|
+
if (!fs.existsSync(skillsDir))
|
|
315
|
+
return;
|
|
316
|
+
fs.mkdirSync(globalCommandsDir, { recursive: true });
|
|
317
|
+
const skills = fs.readdirSync(skillsDir).filter((f) => f.endsWith('.md'));
|
|
318
|
+
const validGlobalFiles = new Set();
|
|
319
|
+
let installed = 0;
|
|
320
|
+
for (const file of skills) {
|
|
321
|
+
validGlobalFiles.add(file);
|
|
322
|
+
const skillName = file.replace('.md', '');
|
|
323
|
+
const skillContent = fs.readFileSync(path.join(skillsDir, file), 'utf-8');
|
|
324
|
+
const cmdContent = buildCommandContent(skillContent, skillName);
|
|
325
|
+
if (safeWriteCommand(path.join(globalCommandsDir, file), cmdContent)) {
|
|
326
|
+
installed++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const removedGlobal = validGlobalFiles.size > 0
|
|
330
|
+
? cleanupStaleCommands(globalCommandsDir, validGlobalFiles)
|
|
331
|
+
: 0;
|
|
332
|
+
log.debug(`슬래시 명령 설치: ${installed}개 설치, ${removedGlobal}개 정리`);
|
|
333
|
+
}
|
|
334
|
+
// ── Rule File Injection ──
|
|
335
|
+
function injectClaudeRuleFiles(cwd, ruleFiles) {
|
|
336
|
+
const PER_RULE_CAP = RULE_FILE_CAPS.perRuleFile;
|
|
337
|
+
const TOTAL_CAP = RULE_FILE_CAPS.totalRuleFiles;
|
|
338
|
+
const globalRulesDir = path.join(os.homedir(), '.claude', 'rules');
|
|
339
|
+
const projectRulesDir = path.join(cwd, '.claude', 'rules');
|
|
340
|
+
fs.mkdirSync(globalRulesDir, { recursive: true });
|
|
341
|
+
fs.mkdirSync(projectRulesDir, { recursive: true });
|
|
342
|
+
let totalWritten = 0;
|
|
343
|
+
for (const [filename, content] of Object.entries(ruleFiles)) {
|
|
344
|
+
const capped = content.length > PER_RULE_CAP
|
|
345
|
+
? `${content.slice(0, PER_RULE_CAP)}\n... (capped at rule file limit)\n`
|
|
346
|
+
: content;
|
|
347
|
+
if (totalWritten + capped.length > TOTAL_CAP) {
|
|
348
|
+
log.debug(`rules/ 총량 캡 도달, ${filename} 생략`);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
const isUserPreference = filename.startsWith('forge-');
|
|
352
|
+
const targetDir = isUserPreference ? globalRulesDir : projectRulesDir;
|
|
353
|
+
fs.writeFileSync(path.join(targetDir, filename), capped);
|
|
354
|
+
totalWritten += capped.length;
|
|
355
|
+
}
|
|
356
|
+
// 마이그레이션: 이전 위치 파일 제거
|
|
357
|
+
const legacyPath = path.join(cwd, '.claude', 'compound-rules.md');
|
|
358
|
+
if (fs.existsSync(legacyPath)) {
|
|
359
|
+
try {
|
|
360
|
+
fs.unlinkSync(legacyPath);
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
log.debug('레거시 규칙 파일 삭제 실패', e);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// CLAUDE.md에서 이전 마커 블록 제거
|
|
367
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
368
|
+
const marker = '<!-- forgen:start -->';
|
|
369
|
+
const endMarker = '<!-- forgen:end -->';
|
|
370
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
371
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
372
|
+
if (content.includes(marker)) {
|
|
373
|
+
const regex = new RegExp(`\\n*${marker}[\\s\\S]*?${endMarker}\\n*`, 'g');
|
|
374
|
+
const cleaned = content
|
|
375
|
+
.replace(regex, '\n')
|
|
376
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
377
|
+
.trim();
|
|
378
|
+
fs.writeFileSync(claudeMdPath, cleaned ? `${cleaned}\n` : '');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// ── Compound Memory ──
|
|
383
|
+
function ensureCompoundMemory(cwd) {
|
|
384
|
+
try {
|
|
385
|
+
const sanitized = cwd.replace(/\//g, '-').replace(/^-/, '');
|
|
386
|
+
const memoryDir = path.join(os.homedir(), '.claude', 'projects', sanitized, 'memory');
|
|
387
|
+
if (!fs.existsSync(memoryDir))
|
|
388
|
+
return;
|
|
389
|
+
const memoryMdPath = path.join(memoryDir, 'MEMORY.md');
|
|
390
|
+
const compoundPointer = '- [Compound Knowledge](compound-index.md) — accumulated patterns/solutions from past sessions';
|
|
391
|
+
if (fs.existsSync(memoryMdPath)) {
|
|
392
|
+
const content = fs.readFileSync(memoryMdPath, 'utf-8');
|
|
393
|
+
if (content.includes('compound-index.md'))
|
|
394
|
+
return;
|
|
395
|
+
fs.writeFileSync(memoryMdPath, content.trimEnd() + '\n' + compoundPointer + '\n');
|
|
396
|
+
}
|
|
397
|
+
const indexPath = path.join(memoryDir, 'compound-index.md');
|
|
398
|
+
const solutionsDir = ME_SOLUTIONS;
|
|
399
|
+
let solutionCount = 0;
|
|
400
|
+
try {
|
|
401
|
+
solutionCount = fs.readdirSync(solutionsDir).filter(f => f.endsWith('.md')).length;
|
|
402
|
+
}
|
|
403
|
+
catch { /* solutions dir may not exist */ }
|
|
404
|
+
const indexContent = [
|
|
405
|
+
'---',
|
|
406
|
+
'name: compound-knowledge-index',
|
|
407
|
+
'description: Forgen compound knowledge — use compound-search MCP tool to find relevant patterns',
|
|
408
|
+
'type: reference',
|
|
409
|
+
'---',
|
|
410
|
+
'',
|
|
411
|
+
`${solutionCount} accumulated solutions available via forgen-compound MCP tools.`,
|
|
412
|
+
'',
|
|
413
|
+
'Use compound-search to find relevant patterns before starting tasks.',
|
|
414
|
+
'Use compound-read to get full solution content.',
|
|
415
|
+
].join('\n');
|
|
416
|
+
fs.writeFileSync(indexPath, indexContent);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// auto memory 접근 실패는 무시
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ── Gitignore ──
|
|
423
|
+
function ensureGitignore(cwd) {
|
|
424
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
425
|
+
const forgenEntries = [
|
|
426
|
+
'# Forgen (auto-generated, do not commit)',
|
|
427
|
+
'.claude/agents/ch-*.md',
|
|
428
|
+
'.claude/agents/pack-*.md',
|
|
429
|
+
'.claude/rules/project-context.md',
|
|
430
|
+
'.claude/rules/routing.md',
|
|
431
|
+
'.claude/rules/forge-*.md',
|
|
432
|
+
'.claude/rules/v1-rules.md',
|
|
433
|
+
'.compound/project-map.json',
|
|
434
|
+
'.claude/commands/forgen/',
|
|
435
|
+
'.compound/notepad.md',
|
|
436
|
+
];
|
|
437
|
+
const marker = '.claude/agents/ch-*.md';
|
|
438
|
+
try {
|
|
439
|
+
let content = '';
|
|
440
|
+
if (fs.existsSync(gitignorePath)) {
|
|
441
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
442
|
+
if (content.includes(marker))
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const newContent = `${content.trimEnd()}\n\n${forgenEntries.join('\n')}\n`;
|
|
446
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// .gitignore 쓰기 실패는 무시
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// ── Main Harness ──
|
|
453
|
+
/** ~/.tenetx/ 및 ~/.compound/ → ~/.forgen/ 스토리지 마이그레이션 */
|
|
454
|
+
function migrateToForgen() {
|
|
455
|
+
const home = os.homedir();
|
|
456
|
+
if (home.startsWith('/tmp/') || home.includes('forgen-test'))
|
|
457
|
+
return;
|
|
458
|
+
const forgenHome = path.join(home, '.forgen');
|
|
459
|
+
const legacyDirs = [
|
|
460
|
+
path.join(home, '.tenetx'),
|
|
461
|
+
path.join(home, '.compound'),
|
|
462
|
+
];
|
|
463
|
+
for (const legacyHome of legacyDirs) {
|
|
464
|
+
try {
|
|
465
|
+
if (fs.lstatSync(legacyHome).isSymbolicLink())
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (!fs.existsSync(legacyHome) || !fs.statSync(legacyHome).isDirectory())
|
|
472
|
+
continue;
|
|
473
|
+
fs.mkdirSync(forgenHome, { recursive: true });
|
|
474
|
+
try {
|
|
475
|
+
const entries = fs.readdirSync(legacyHome, { withFileTypes: true });
|
|
476
|
+
for (const entry of entries) {
|
|
477
|
+
const src = path.join(legacyHome, entry.name);
|
|
478
|
+
const dest = path.join(forgenHome, entry.name);
|
|
479
|
+
if (fs.existsSync(dest))
|
|
480
|
+
continue;
|
|
481
|
+
if (entry.isDirectory()) {
|
|
482
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
483
|
+
}
|
|
484
|
+
else if (entry.isFile()) {
|
|
485
|
+
fs.copyFileSync(src, dest);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
log.debug(`migrateToForgen: ${legacyHome} 파일 복사 중 오류`, e);
|
|
491
|
+
}
|
|
492
|
+
const backupPath = legacyHome + '.bak';
|
|
493
|
+
try {
|
|
494
|
+
if (!fs.existsSync(backupPath)) {
|
|
495
|
+
fs.renameSync(legacyHome, backupPath);
|
|
496
|
+
fs.symlinkSync(forgenHome, legacyHome, 'dir');
|
|
497
|
+
log.debug(`migrateToForgen: ${legacyHome} → ~/.forgen symlink 생성 완료`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
log.debug(`migrateToForgen: ${legacyHome} symlink 생성 실패`, e);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// 레거시 디렉토리가 없으면 symlink 생성
|
|
505
|
+
for (const legacyHome of legacyDirs) {
|
|
506
|
+
if (!fs.existsSync(legacyHome)) {
|
|
507
|
+
try {
|
|
508
|
+
fs.symlinkSync(forgenHome, legacyHome, 'dir');
|
|
509
|
+
}
|
|
510
|
+
catch { /* ignore */ }
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/** 메인 하네스 준비 함수 (v1) */
|
|
515
|
+
// ── B9: prepareHarness sub-phases (extracted steps 11-12) ──
|
|
516
|
+
/** Step 11: start legacy session log (fail-open). */
|
|
517
|
+
async function startLegacySessionLog(cwd, inTmux, v1Result) {
|
|
518
|
+
try {
|
|
519
|
+
const { startSessionLog: legacySessionLog } = await import('./session-logger.js');
|
|
520
|
+
legacySessionLog({
|
|
521
|
+
philosophy: { name: 'v1', version: '1.0.0', author: 'forgen', principles: {} },
|
|
522
|
+
philosophySource: 'default',
|
|
523
|
+
scope: {
|
|
524
|
+
me: { philosophyPath: '', solutionCount: 0, ruleCount: 0 },
|
|
525
|
+
project: { path: cwd, solutionCount: 0 },
|
|
526
|
+
summary: `v1(${v1Result.session?.quality_pack ?? 'unknown'})`,
|
|
527
|
+
},
|
|
528
|
+
cwd,
|
|
529
|
+
inTmux,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
catch { /* 세션 로그 실패는 무시 */ }
|
|
533
|
+
}
|
|
534
|
+
/** Step 12: write pending-compound.json if last extraction is stale. */
|
|
535
|
+
function checkCompoundStaleness() {
|
|
536
|
+
try {
|
|
537
|
+
const stalenessDays = Number(process.env.FORGEN_STALENESS_DAYS ?? process.env.COMPOUND_STALENESS_DAYS) || 3;
|
|
538
|
+
const stalenessMs = stalenessDays * 24 * 60 * 60 * 1000;
|
|
539
|
+
const lastExtractionPath = path.join(STATE_DIR, 'last-extraction.json');
|
|
540
|
+
if (!fs.existsSync(lastExtractionPath))
|
|
541
|
+
return;
|
|
542
|
+
const lastExtraction = JSON.parse(fs.readFileSync(lastExtractionPath, 'utf-8'));
|
|
543
|
+
const extractedAt = lastExtraction.lastExtractedAt ?? lastExtraction.lastRunAt;
|
|
544
|
+
const lastRunMs = extractedAt ? new Date(extractedAt).getTime() : Number.NaN;
|
|
545
|
+
if (!Number.isFinite(lastRunMs))
|
|
546
|
+
return;
|
|
547
|
+
const elapsed = Date.now() - lastRunMs;
|
|
548
|
+
if (elapsed > stalenessMs) {
|
|
549
|
+
const pendingPath = path.join(STATE_DIR, 'pending-compound.json');
|
|
550
|
+
if (!fs.existsSync(pendingPath)) {
|
|
551
|
+
fs.writeFileSync(pendingPath, JSON.stringify({
|
|
552
|
+
reason: 'staleness',
|
|
553
|
+
detectedAt: new Date().toISOString(),
|
|
554
|
+
daysSinceLastRun: Math.floor(elapsed / (24 * 60 * 60 * 1000)),
|
|
555
|
+
}, null, 2));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (e) {
|
|
560
|
+
log.debug('Staleness check failed (non-fatal)', e);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
export async function prepareHarness(cwd) {
|
|
564
|
+
try {
|
|
565
|
+
// 0. 스토리지 마이그레이션 (v5.1: ~/.compound/ → ~/.forgen/)
|
|
566
|
+
migrateToForgen();
|
|
567
|
+
// 1. 디렉토리 구조 보장
|
|
568
|
+
ensureDirectories();
|
|
569
|
+
// 2. v1 Session Bootstrap (legacy 감지 → profile 로드 → preset 합성 → rule 렌더)
|
|
570
|
+
const v1Result = bootstrapV1Session();
|
|
571
|
+
if (v1Result.needsOnboarding) {
|
|
572
|
+
log.debug('v1: 온보딩 필요 — forgen onboarding 실행 안내');
|
|
573
|
+
}
|
|
574
|
+
if (v1Result.legacyBackupPath) {
|
|
575
|
+
log.debug(`v1: 레거시 프로필 백업 완료 → ${v1Result.legacyBackupPath}`);
|
|
576
|
+
}
|
|
577
|
+
if (v1Result.session) {
|
|
578
|
+
const { session } = v1Result;
|
|
579
|
+
log.debug(`v1 세션 시작: ${session.quality_pack}/${session.autonomy_pack}, trust=${session.effective_trust_policy}`);
|
|
580
|
+
for (const w of session.warnings) {
|
|
581
|
+
// mismatch 경고는 사용자에게 직접 표시
|
|
582
|
+
if (w.includes('mismatch')) {
|
|
583
|
+
console.error(`[forgen] ${w}`);
|
|
584
|
+
}
|
|
585
|
+
log.debug(`v1 경고: ${w}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (v1Result.mismatch?.quality_mismatch || v1Result.mismatch?.autonomy_mismatch) {
|
|
589
|
+
log.debug(`v1 mismatch 감지: quality=${v1Result.mismatch.quality_score}, autonomy=${v1Result.mismatch.autonomy_score}`);
|
|
590
|
+
}
|
|
591
|
+
// 3. 환경 확인
|
|
592
|
+
const inTmux = !!process.env.TMUX;
|
|
593
|
+
// 4. Claude Code 설정 주입 (환경변수 + trust 기반 permissions)
|
|
594
|
+
const env = buildEnv(cwd, v1Result.session?.session_id);
|
|
595
|
+
injectSettings(env, v1Result);
|
|
596
|
+
// 5. 에이전트 설치
|
|
597
|
+
installAgents(cwd);
|
|
598
|
+
// 6. 규칙 파일 생성 및 주입 (v1 부트스트랩 결과의 renderedRules를 직접 전달)
|
|
599
|
+
const ruleFiles = generateClaudeRuleFiles(cwd, v1Result.renderedRules);
|
|
600
|
+
injectClaudeRuleFiles(cwd, ruleFiles);
|
|
601
|
+
// 7. 슬래시 명령 설치
|
|
602
|
+
installSlashCommands(cwd);
|
|
603
|
+
// 8. tmux 바인딩 등록
|
|
604
|
+
if (inTmux) {
|
|
605
|
+
await registerTmuxBindings();
|
|
606
|
+
}
|
|
607
|
+
// 9. .gitignore 등록
|
|
608
|
+
ensureGitignore(cwd);
|
|
609
|
+
// 10. Auto memory에 compound 포인터 추가
|
|
610
|
+
ensureCompoundMemory(cwd);
|
|
611
|
+
// 11. 세션 로그 시작 (레거시 호환)
|
|
612
|
+
await startLegacySessionLog(cwd, inTmux, v1Result);
|
|
613
|
+
// 12. Compound staleness guard
|
|
614
|
+
checkCompoundStaleness();
|
|
615
|
+
return { cwd, inTmux, v1: v1Result };
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
rollbackSettings();
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen init — v1 프로젝트 초기화
|
|
3
|
+
*
|
|
4
|
+
* 온보딩 기반 프로필 생성 + v1 디렉토리 구조 초기화.
|
|
5
|
+
* philosophy/pack 시스템은 v1에서 제거됨.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { profileExists } from '../store/profile-store.js';
|
|
10
|
+
import { ensureV1Directories } from './v1-bootstrap.js';
|
|
11
|
+
// ── CLI 핸들러 ──
|
|
12
|
+
export async function handleInit(_args) {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const projectName = path.basename(cwd);
|
|
15
|
+
console.log(`\n Forgen Init — ${projectName}\n`);
|
|
16
|
+
// v1 디렉토리 생성
|
|
17
|
+
ensureV1Directories();
|
|
18
|
+
// 프로젝트 .claude/rules 디렉토리 생성
|
|
19
|
+
const rulesDir = path.join(cwd, '.claude', 'rules');
|
|
20
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
21
|
+
// 프로필 존재 확인
|
|
22
|
+
if (profileExists()) {
|
|
23
|
+
console.log(' Profile already exists. Your personalization is active.');
|
|
24
|
+
console.log(' Run `forgen inspect profile` to view your current settings.');
|
|
25
|
+
console.log(' Run `forgen forge --reset` to re-onboard.\n');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(' No profile found. Starting onboarding...\n');
|
|
29
|
+
// 온보딩 실행
|
|
30
|
+
const { runOnboarding } = await import('../forge/onboarding-cli.js');
|
|
31
|
+
await runOnboarding();
|
|
32
|
+
console.log(' Init complete!');
|
|
33
|
+
console.log(' Next steps:');
|
|
34
|
+
console.log(' forgen Start Claude Code with personalization');
|
|
35
|
+
console.log(' forgen inspect profile View your profile');
|
|
36
|
+
console.log(' forgen doctor Check system health\n');
|
|
37
|
+
}
|