@wooojin/forgen 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.claude-plugin/plugin.json +5 -5
  2. package/CHANGELOG.md +194 -15
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.ja.md +74 -9
  5. package/README.ko.md +77 -12
  6. package/README.md +127 -25
  7. package/README.zh.md +43 -9
  8. package/assets/README.md +86 -0
  9. package/assets/architecture.svg +100 -0
  10. package/assets/banner.png +0 -0
  11. package/assets/banner.svg +53 -0
  12. package/assets/demo/01-install.gif +0 -0
  13. package/assets/demo/01-install.tape +54 -0
  14. package/assets/demo/02-compound-learning.gif +0 -0
  15. package/assets/demo/02-compound-learning.tape +50 -0
  16. package/assets/demo/03-forge-personalization.gif +0 -0
  17. package/assets/demo/03-forge-personalization.tape +64 -0
  18. package/assets/demo/before-after.gif +0 -0
  19. package/assets/demo/before-after.tape +98 -0
  20. package/assets/demo-preview.svg +96 -0
  21. package/assets/icon.png +0 -0
  22. package/{hooks → assets/shared}/hook-registry.json +2 -1
  23. package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
  24. package/dist/checks/conclusion-verification-ratio.js +86 -0
  25. package/dist/checks/fact-vs-agreement.d.ts +47 -0
  26. package/dist/checks/fact-vs-agreement.js +92 -0
  27. package/dist/checks/self-score-deflation.d.ts +38 -0
  28. package/dist/checks/self-score-deflation.js +108 -0
  29. package/dist/cli.js +98 -6
  30. package/dist/core/auto-compound-runner.js +137 -49
  31. package/dist/core/behavior-classifier.d.ts +28 -0
  32. package/dist/core/behavior-classifier.js +46 -0
  33. package/dist/core/dashboard.d.ts +7 -0
  34. package/dist/core/dashboard.js +41 -2
  35. package/dist/core/doctor.js +118 -5
  36. package/dist/core/extraction-notice.d.ts +18 -0
  37. package/dist/core/extraction-notice.js +64 -0
  38. package/dist/core/git-stats.d.ts +36 -0
  39. package/dist/core/git-stats.js +79 -0
  40. package/dist/core/harness.d.ts +1 -1
  41. package/dist/core/harness.js +27 -20
  42. package/dist/core/host-detect.d.ts +42 -0
  43. package/dist/core/host-detect.js +68 -0
  44. package/dist/core/init-cli.d.ts +26 -0
  45. package/dist/core/init-cli.js +104 -0
  46. package/dist/core/init.js +17 -0
  47. package/dist/core/inspect-cli.js +1 -2
  48. package/dist/core/installer.js +2 -2
  49. package/dist/core/migrate-cli.d.ts +11 -0
  50. package/dist/core/migrate-cli.js +53 -0
  51. package/dist/core/migrate-evidence-host.d.ts +36 -0
  52. package/dist/core/migrate-evidence-host.js +49 -0
  53. package/dist/core/paths.d.ts +8 -1
  54. package/dist/core/paths.js +11 -2
  55. package/dist/core/recall-cli.d.ts +26 -0
  56. package/dist/core/recall-cli.js +125 -0
  57. package/dist/core/recall-reference-detector.d.ts +43 -0
  58. package/dist/core/recall-reference-detector.js +65 -0
  59. package/dist/core/settings-injector.js +4 -2
  60. package/dist/core/spawn.d.ts +1 -1
  61. package/dist/core/spawn.js +4 -11
  62. package/dist/core/stats-cli.d.ts +21 -0
  63. package/dist/core/stats-cli.js +133 -10
  64. package/dist/core/trust-layer-intent.d.ts +35 -0
  65. package/dist/core/trust-layer-intent.js +30 -0
  66. package/dist/core/types.d.ts +1 -1
  67. package/dist/core/uninstall.js +2 -1
  68. package/dist/engine/compound-cli.js +1 -0
  69. package/dist/engine/compound-export.js +8 -3
  70. package/dist/engine/compound-extractor.js +7 -9
  71. package/dist/engine/learn-cli.js +5 -6
  72. package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
  73. package/dist/engine/lifecycle/bypass-detector.js +57 -5
  74. package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
  75. package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
  76. package/dist/engine/lifecycle/orchestrator.js +2 -2
  77. package/dist/engine/lifecycle/signals.js +6 -6
  78. package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
  79. package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
  80. package/dist/engine/skill-promoter.js +3 -6
  81. package/dist/fgx.js +2 -1
  82. package/dist/forge/evidence-processor.js +12 -0
  83. package/dist/forge/onboarding.d.ts +3 -2
  84. package/dist/forge/onboarding.js +3 -2
  85. package/dist/hooks/context-guard.js +1 -1
  86. package/dist/hooks/dangerous-patterns.json +3 -3
  87. package/dist/hooks/db-guard.js +21 -5
  88. package/dist/hooks/forge-loop-progress.d.ts +9 -0
  89. package/dist/hooks/forge-loop-progress.js +38 -0
  90. package/dist/hooks/hook-registry.js +1 -1
  91. package/dist/hooks/hooks-generator.d.ts +15 -1
  92. package/dist/hooks/hooks-generator.js +18 -16
  93. package/dist/hooks/intent-classifier.js +1 -1
  94. package/dist/hooks/keyword-detector.js +2 -2
  95. package/dist/hooks/notepad-injector.js +1 -1
  96. package/dist/hooks/permission-handler.js +1 -1
  97. package/dist/hooks/post-tool-failure.js +1 -1
  98. package/dist/hooks/post-tool-use.d.ts +7 -1
  99. package/dist/hooks/post-tool-use.js +50 -23
  100. package/dist/hooks/pre-compact.js +2 -2
  101. package/dist/hooks/pre-tool-use.d.ts +7 -0
  102. package/dist/hooks/pre-tool-use.js +28 -10
  103. package/dist/hooks/rate-limiter.js +3 -3
  104. package/dist/hooks/secret-filter.js +1 -1
  105. package/dist/hooks/session-recovery.js +12 -1
  106. package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
  107. package/dist/hooks/shared/blocking-allowlist.js +38 -0
  108. package/dist/hooks/shared/command-parser.d.ts +44 -0
  109. package/dist/hooks/shared/command-parser.js +50 -0
  110. package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
  111. package/dist/hooks/shared/forge-loop-state.js +116 -0
  112. package/dist/hooks/shared/hook-response.d.ts +30 -2
  113. package/dist/hooks/shared/hook-response.js +61 -3
  114. package/dist/hooks/skill-injector.js +2 -2
  115. package/dist/hooks/slop-detector.js +2 -2
  116. package/dist/hooks/solution-injector.d.ts +9 -0
  117. package/dist/hooks/solution-injector.js +48 -5
  118. package/dist/hooks/stop-guard.js +152 -13
  119. package/dist/hooks/subagent-tracker.js +1 -1
  120. package/dist/host/capabilities-claude.d.ts +8 -0
  121. package/dist/host/capabilities-claude.js +46 -0
  122. package/dist/host/capabilities-codex.d.ts +11 -0
  123. package/dist/host/capabilities-codex.js +50 -0
  124. package/dist/host/capabilities-registry.d.ts +11 -0
  125. package/dist/host/capabilities-registry.js +30 -0
  126. package/dist/host/codex-adapter.d.ts +8 -5
  127. package/dist/host/codex-adapter.js +10 -82
  128. package/dist/host/codex-output-parser.d.ts +39 -0
  129. package/dist/host/codex-output-parser.js +75 -0
  130. package/dist/host/exec-host.d.ts +54 -0
  131. package/dist/host/exec-host.js +92 -0
  132. package/dist/host/host-runtime.d.ts +37 -0
  133. package/dist/host/host-runtime.js +51 -0
  134. package/dist/host/install-claude.d.ts +35 -0
  135. package/dist/host/install-claude.js +238 -0
  136. package/dist/host/install-codex.d.ts +44 -0
  137. package/dist/host/install-codex.js +276 -0
  138. package/dist/host/install-orchestrator.d.ts +34 -0
  139. package/dist/host/install-orchestrator.js +126 -0
  140. package/dist/host/invoke-agent.d.ts +27 -0
  141. package/dist/host/invoke-agent.js +115 -0
  142. package/dist/host/parity-harness.d.ts +62 -0
  143. package/dist/host/parity-harness.js +283 -0
  144. package/dist/host/projection.d.ts +35 -0
  145. package/dist/host/projection.js +126 -0
  146. package/dist/i18n/index.js +3 -5
  147. package/dist/mcp/server.js +11 -0
  148. package/dist/mcp/tools.js +47 -0
  149. package/dist/services/session.d.ts +6 -3
  150. package/dist/services/session.js +33 -4
  151. package/dist/store/evidence-store.d.ts +1 -0
  152. package/dist/store/evidence-store.js +45 -3
  153. package/dist/store/host-mismatch.d.ts +42 -0
  154. package/dist/store/host-mismatch.js +65 -0
  155. package/dist/store/implicit-feedback-store.d.ts +59 -0
  156. package/dist/store/implicit-feedback-store.js +153 -0
  157. package/dist/store/profile-store.d.ts +29 -0
  158. package/dist/store/profile-store.js +53 -0
  159. package/dist/store/rule-store.js +8 -0
  160. package/dist/store/types.d.ts +13 -0
  161. package/hooks/hooks.json +6 -1
  162. package/package.json +7 -5
  163. package/plugin.json +4 -4
  164. package/scripts/postinstall.js +100 -25
  165. /package/{agents → assets/claude/agents}/analyst.md +0 -0
  166. /package/{agents → assets/claude/agents}/architect.md +0 -0
  167. /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
  168. /package/{agents → assets/claude/agents}/critic.md +0 -0
  169. /package/{agents → assets/claude/agents}/debugger.md +0 -0
  170. /package/{agents → assets/claude/agents}/designer.md +0 -0
  171. /package/{agents → assets/claude/agents}/executor.md +0 -0
  172. /package/{agents → assets/claude/agents}/explore.md +0 -0
  173. /package/{agents → assets/claude/agents}/git-master.md +0 -0
  174. /package/{agents → assets/claude/agents}/planner.md +0 -0
  175. /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
  176. /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
  177. /package/{agents → assets/claude/agents}/verifier.md +0 -0
  178. /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
  179. /package/{commands → assets/claude/commands}/calibrate.md +0 -0
  180. /package/{commands → assets/claude/commands}/code-review.md +0 -0
  181. /package/{commands → assets/claude/commands}/compound.md +0 -0
  182. /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
  183. /package/{commands → assets/claude/commands}/docker.md +0 -0
  184. /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
  185. /package/{commands → assets/claude/commands}/learn.md +0 -0
  186. /package/{commands → assets/claude/commands}/retro.md +0 -0
  187. /package/{commands → assets/claude/commands}/ship.md +0 -0
@@ -12,6 +12,7 @@
12
12
  * - state/sessions/{sessionId}.json → session metadata
13
13
  */
14
14
  import type { SessionQualityScore } from './types.js';
15
+ import { type ImplicitFeedbackEntry } from '../../store/implicit-feedback-store.js';
15
16
  interface InjectionCacheData {
16
17
  injected: string[];
17
18
  totalInjectedChars: number;
@@ -29,12 +30,6 @@ interface DriftState {
29
30
  lastCriticalAt: number | null;
30
31
  hardCapReached: boolean;
31
32
  }
32
- interface ImplicitFeedbackEntry {
33
- type: string;
34
- sessionId?: string;
35
- at: string;
36
- [key: string]: unknown;
37
- }
38
33
  export declare function loadInjectionCache(sessionId: string): InjectionCacheData | null;
39
34
  export declare function loadDriftState(sessionId: string): DriftState | null;
40
35
  export declare function loadImplicitFeedback(sessionId: string): ImplicitFeedbackEntry[];
@@ -15,6 +15,7 @@ import * as fs from 'node:fs';
15
15
  import * as path from 'node:path';
16
16
  import { ME_BEHAVIOR, STATE_DIR } from '../../core/paths.js';
17
17
  import { safeReadJSON } from '../../hooks/shared/atomic-write.js';
18
+ import { loadImplicitFeedback as loadImplicitFeedbackFromStore, } from '../../store/implicit-feedback-store.js';
18
19
  function sanitizeId(id) {
19
20
  return id.replace(/[^a-zA-Z0-9_-]/g, '_');
20
21
  }
@@ -32,27 +33,7 @@ export function loadDriftState(sessionId) {
32
33
  return data?.drift ?? null;
33
34
  }
34
35
  export function loadImplicitFeedback(sessionId) {
35
- const logPath = path.join(STATE_DIR, 'implicit-feedback.jsonl');
36
- try {
37
- if (!fs.existsSync(logPath))
38
- return [];
39
- const lines = fs.readFileSync(logPath, 'utf-8').split('\n').filter(Boolean);
40
- const entries = [];
41
- for (const line of lines) {
42
- try {
43
- const entry = JSON.parse(line);
44
- if (entry.sessionId === sessionId)
45
- entries.push(entry);
46
- }
47
- catch {
48
- /* skip malformed lines */
49
- }
50
- }
51
- return entries;
52
- }
53
- catch {
54
- return [];
55
- }
36
+ return loadImplicitFeedbackFromStore(sessionId);
56
37
  }
57
38
  export function loadSessionCorrections(sessionId) {
58
39
  try {
@@ -6,15 +6,12 @@
6
6
  */
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
- import * as os from 'node:os';
10
9
  import { parseSolutionV3 } from './solution-format.js';
11
10
  import { createLogger } from '../core/logger.js';
11
+ import { ME_SOLUTIONS, ME_SKILLS, CLAUDE_DIR } from '../core/paths.js';
12
12
  const log = createLogger('skill-promoter');
13
- const FORGEN_HOME = path.join(os.homedir(), '.forgen');
14
- const ME_SOLUTIONS = path.join(FORGEN_HOME, 'me', 'solutions');
15
- const ME_SKILLS = path.join(FORGEN_HOME, 'me', 'skills');
16
- // Claude Code가 자동 인식하는 글로벌 스킬 경로
17
- const CLAUDE_SKILLS = path.join(os.homedir(), '.claude', 'skills');
13
+ // Claude Code가 자동 인식하는 글로벌 스킬 경로 (~/.claude/skills)
14
+ const CLAUDE_SKILLS = path.join(CLAUDE_DIR, 'skills');
18
15
  // 일반적인 태그 제외 (트리거로 부적합)
19
16
  const GENERIC_TAGS = new Set([
20
17
  'typescript', 'javascript', 'react', 'node', 'error', 'fix', 'code',
package/dist/fgx.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import { resolveLaunchContext } from './services/session.js';
7
7
  import { prepareHarness, isFirstRun } from './core/harness.js';
8
8
  import { spawnClaude } from './core/spawn.js';
9
+ import { getHostRuntime } from './host/host-runtime.js';
9
10
  const args = process.argv.slice(2);
10
11
  // 이미 포함되어 있으면 중복 추가하지 않음
11
12
  const launchContext = resolveLaunchContext(args);
@@ -43,7 +44,7 @@ async function main() {
43
44
  console.log(`[forgen] Trust: ${v1.session.effective_trust_policy}`);
44
45
  }
45
46
  console.log('[forgen] Mode: dangerously-skip-permissions');
46
- const runtimeLabel = runtime === 'codex' ? 'Codex' : 'Claude';
47
+ const runtimeLabel = getHostRuntime(runtime).displayName;
47
48
  console.log(`[forgen] Starting ${runtimeLabel}...\n`);
48
49
  await spawnClaude(launchArgs, context, runtime);
49
50
  }
@@ -8,6 +8,7 @@
8
8
  import { createEvidence, appendEvidence } from '../store/evidence-store.js';
9
9
  import { createRule, saveRule } from '../store/rule-store.js';
10
10
  import { classify, applyProposal } from '../engine/enforce-classifier.js';
11
+ import { bumpAxisConfidence } from '../store/profile-store.js';
11
12
  // ── Correction → Evidence + Temporary Rule ──
12
13
  /**
13
14
  * 사용자 교정을 Evidence로 기록하고, 필요 시 temporary rule 생성.
@@ -31,6 +32,17 @@ export function processCorrection(req) {
31
32
  },
32
33
  });
33
34
  appendEvidence(evidence); // T1 lifecycle trigger fires here for explicit_correction
35
+ // D2 fix (2026-04-27): explicit_correction 의 axis_hint 가 axes confidence 에
36
+ // 직접 반영되도록 bump. autonomy 6건이 score 못 움직였던 결함 해결.
37
+ // 회귀 안전: facet 값은 안 건드리고 confidence 만 +0.02 (avoid-this 는 +0.04
38
+ // 로 더 강한 신호). docs/issues/D2-autonomy-facet-stuck.md 참조.
39
+ if (req.axis_hint) {
40
+ const bump = req.kind === 'avoid-this' ? 0.04 : 0.02;
41
+ try {
42
+ bumpAxisConfidence(req.axis_hint, bump);
43
+ }
44
+ catch { /* fail-open */ }
45
+ }
34
46
  // fix-now, avoid-this → temporary session rule
35
47
  let temporaryRule = null;
36
48
  if (req.kind === 'fix-now' || req.kind === 'avoid-this') {
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Forgen v1 — Onboarding
3
3
  *
4
- * 2문항 온보딩, 점수 계산, pack 추천.
5
- * Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §3-4
4
+ * 4문항 온보딩 (quality / autonomy / judgment / communication 4축), 점수 계산, pack 추천.
5
+ * Authoritative spec: docs/history/2026-04-03-tenetx-onboarding-adaptation-spec.md
6
+ * (spec §3 은 v0.1 시점 2문항 기준 — v0.4 부터 4문항으로 확장됨, src/forge/onboarding-cli.ts:69-75 참조)
6
7
  */
7
8
  import type { QualityPack, AutonomyPack, JudgmentPack, CommunicationPack, TrustPolicy, PackRecommendation } from '../store/types.js';
8
9
  export type ChoiceId = 'A' | 'B' | 'C';
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Forgen v1 — Onboarding
3
3
  *
4
- * 2문항 온보딩, 점수 계산, pack 추천.
5
- * Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §3-4
4
+ * 4문항 온보딩 (quality / autonomy / judgment / communication 4축), 점수 계산, pack 추천.
5
+ * Authoritative spec: docs/history/2026-04-03-tenetx-onboarding-adaptation-spec.md
6
+ * (spec §3 은 v0.1 시점 2문항 기준 — v0.4 부터 4문항으로 확장됨, src/forge/onboarding-cli.ts:69-75 참조)
6
7
  */
7
8
  import { createRecommendation } from '../store/recommendation-store.js';
8
9
  // 질문 1: 애매한 구현 요청 + 인접 영향 가능성
@@ -408,6 +408,6 @@ function saveHandoff(sessionId, reason, detail) {
408
408
  if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
409
409
  main().catch((e) => {
410
410
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
411
- console.log(failOpenWithTracking('context-guard'));
411
+ console.log(failOpenWithTracking('context-guard', e));
412
412
  });
413
413
  }
@@ -1,5 +1,5 @@
1
1
  [
2
- { "pattern": "rm\\s+(-rf|-fr)\\s+[/~]", "description": "rm -rf on root/home path", "severity": "block" },
2
+ { "pattern": "rm\\s+(-rf|-fr)\\s+(\\/(?!tmp\\b|var\\/folders\\b|var\\/tmp\\b)|~)", "description": "rm -rf on root/home path", "severity": "block" },
3
3
  { "pattern": "rm\\s+(-rf|-fr)\\s+\\.\\s", "description": "rm -rf on current directory", "severity": "block" },
4
4
  { "pattern": "git\\s+push\\s+.*--force(?!-)", "description": "git push --force", "severity": "warn" },
5
5
  { "pattern": "git\\s+reset\\s+--hard", "description": "git reset --hard", "severity": "warn" },
@@ -9,10 +9,10 @@
9
9
  { "pattern": ">\\s*\\/dev\\/sd[a-z]", "description": "write to block device", "severity": "block" },
10
10
  { "pattern": "mkfs\\s", "description": "mkfs (format filesystem)", "severity": "block" },
11
11
  { "pattern": ":\\(\\)\\s*\\{\\s*:\\|:&\\s*\\}\\s*;:", "description": "fork bomb", "severity": "block" },
12
- { "pattern": "\\beval\\s+[\"'`]", "description": "eval with string (injection risk)", "severity": "warn" },
12
+ { "pattern": "\\beval\\s+[\"'`]", "description": "eval with string (injection risk)", "severity": "warn", "match_target": "raw" },
13
13
  { "pattern": "curl\\s+.*\\|\\s*(ba)?sh", "description": "curl pipe to shell", "severity": "block" },
14
14
  { "pattern": "wget\\s+.*\\|\\s*(ba)?sh", "description": "wget pipe to shell", "severity": "block" },
15
- { "pattern": "python[23]?\\s+-c\\s+['\"].*(?:import\\s+os|subprocess|exec|eval)", "description": "python -c with dangerous imports", "severity": "warn" },
15
+ { "pattern": "python[23]?\\s+-c\\s+['\"].*(?:import\\s+os|subprocess|exec|eval)", "description": "python -c with dangerous imports", "severity": "warn", "match_target": "raw" },
16
16
  { "pattern": "\\bchmod\\s+[0-7]*777\\b", "description": "chmod 777 (overly permissive)", "severity": "warn" },
17
17
  { "pattern": "\\bdd\\s+.*of=\\/dev\\/", "description": "dd write to device", "severity": "block" }
18
18
  ]
@@ -9,8 +9,9 @@ import * as path from 'node:path';
9
9
  import { readStdinJSON } from './shared/read-stdin.js';
10
10
  import { atomicWriteJSON } from './shared/atomic-write.js';
11
11
  import { isHookEnabled } from './hook-config.js';
12
- import { approve, approveWithWarning, deny, failOpenWithTracking } from './shared/hook-response.js';
12
+ import { approve, approveWithWarning, denyOrObserve, failOpenWithTracking } from './shared/hook-response.js';
13
13
  import { STATE_DIR } from '../core/paths.js';
14
+ import { preprocessForMatch } from './shared/command-parser.js';
14
15
  const FAIL_COUNTER_PATH = path.join(STATE_DIR, 'db-guard-fail-counter.json');
15
16
  const FAIL_CLOSE_THRESHOLD = 3;
16
17
  export const DANGEROUS_SQL_PATTERNS = [
@@ -27,8 +28,23 @@ export function checkDangerousSql(toolName, toolInput) {
27
28
  const command = typeof toolInput === 'string'
28
29
  ? toolInput
29
30
  : (toolInput.command ?? '');
31
+ // TEST-6 확장 (2026-04-24): DB CLI allowlist 기반 quote-aware 전처리.
32
+ //
33
+ // 결함: 이전에는 raw command 를 직접 매칭해 `git commit -m "... DROP TABLE ..."`
34
+ // 같은 quote 안 SQL 키워드까지 block (실증: 이번 세션 내 release 커밋 메시지 차단).
35
+ //
36
+ // 단순히 masked 만 쓰면 `psql -c "DROP TABLE users"` 같은 실 DB 실행의 True-Positive
37
+ // 까지 놓친다. 해법: masked 처리 후에도 **DB CLI 토큰** 이 보이면 진짜 실행 의도
38
+ // 라고 판단해 raw 를 검사, 아니면 masked 를 검사.
39
+ // - `psql -c "DROP TABLE"` → masked: `psql -c ""` → psql 존재 → raw 검사 → block
40
+ // - `git commit -m "DROP TABLE"` → masked: `git commit -m ""` → psql 없음 → masked 검사 → pass
41
+ // - `DROP DATABASE production` (direct SQL) → masked 그대로 (quote 없음) → block
42
+ const maskedCommand = preprocessForMatch(command, 'masked');
43
+ const dbCliRe = /\b(psql|mysql|sqlite3?|pg_restore|mongosh|mysqldump|cockroach\s+sql|redis-cli)\b/i;
44
+ const hasDbCli = dbCliRe.test(maskedCommand);
45
+ const scanCommand = hasDbCli ? command : maskedCommand;
30
46
  // 주석 제거 후 SQL에 대해 패턴 매칭 (주석 안 키워드 오차단 방지)
31
- const sqlWithoutComments = command
47
+ const sqlWithoutComments = scanCommand
32
48
  .replace(/--[^\n]*/g, '') // 라인 주석 제거
33
49
  .replace(/\/\*[\s\S]*?\*\//g, ''); // 블록 주석 제거
34
50
  for (const { pattern, description, severity } of DANGEROUS_SQL_PATTERNS) {
@@ -73,7 +89,7 @@ async function main() {
73
89
  if (!data) {
74
90
  const failCount = getAndIncrementFailCount();
75
91
  if (failCount >= FAIL_CLOSE_THRESHOLD) {
76
- console.log(deny(`[Forgen] DB Guard: stdin parse failed ${failCount} consecutive times — blocking for safety.`));
92
+ console.log(denyOrObserve('db-guard', `[Forgen] DB Guard: stdin parse failed ${failCount} consecutive times — blocking for safety.`));
77
93
  }
78
94
  else {
79
95
  process.stderr.write(`[ch-hook] db-guard stdin parse failed (${failCount}/${FAIL_CLOSE_THRESHOLD})\n`);
@@ -90,7 +106,7 @@ async function main() {
90
106
  const toolInput = data.tool_input ?? data.toolInput ?? {};
91
107
  const check = checkDangerousSql(toolName, toolInput);
92
108
  if (check.action === 'block') {
93
- console.log(deny(`[Forgen] Dangerous SQL blocked: ${check.description}`));
109
+ console.log(denyOrObserve('db-guard', `[Forgen] Dangerous SQL blocked: ${check.description}`));
94
110
  return;
95
111
  }
96
112
  if (check.action === 'warn') {
@@ -101,5 +117,5 @@ async function main() {
101
117
  }
102
118
  main().catch((e) => {
103
119
  process.stderr.write(`[ch-hook] DB Guard error: ${e instanceof Error ? e.message : String(e)}\n`);
104
- console.log(failOpenWithTracking('db-guard'));
120
+ console.log(failOpenWithTracking('db-guard', e));
105
121
  });
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forgen — Forge Loop Progress Injector
4
+ *
5
+ * Claude Code UserPromptSubmit 훅. forge-loop active=true 인 동안 매 프롬프트
6
+ * 마다 진행 상황(N/M, next story)을 컨텍스트에 inject 한다. RC6 가드의 두 번째
7
+ * 축 — 세션 도중에도 forge-loop 가 컨텍스트에서 사라지지 않게 함.
8
+ */
9
+ export {};
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forgen — Forge Loop Progress Injector
4
+ *
5
+ * Claude Code UserPromptSubmit 훅. forge-loop active=true 인 동안 매 프롬프트
6
+ * 마다 진행 상황(N/M, next story)을 컨텍스트에 inject 한다. RC6 가드의 두 번째
7
+ * 축 — 세션 도중에도 forge-loop 가 컨텍스트에서 사라지지 않게 함.
8
+ */
9
+ import { readStdinJSON } from './shared/read-stdin.js';
10
+ import { isHookEnabled } from './hook-config.js';
11
+ import { approve, approveWithContext, failOpenWithTracking } from './shared/hook-response.js';
12
+ import { recordHookTiming } from './shared/hook-timing.js';
13
+ import { readForgeLoopState, renderForgeLoopForPrompt } from './shared/forge-loop-state.js';
14
+ import { createLogger } from '../core/logger.js';
15
+ const log = createLogger('forge-loop-progress');
16
+ async function main() {
17
+ const _hookStart = Date.now();
18
+ try {
19
+ await readStdinJSON().catch((e) => { log.debug('stdin read failed', e); return null; });
20
+ if (!isHookEnabled('forge-loop-progress')) {
21
+ console.log(approve());
22
+ return;
23
+ }
24
+ const block = renderForgeLoopForPrompt(readForgeLoopState());
25
+ if (!block) {
26
+ console.log(approve());
27
+ return;
28
+ }
29
+ console.log(approveWithContext(block, 'UserPromptSubmit'));
30
+ }
31
+ finally {
32
+ recordHookTiming('forge-loop-progress', Date.now() - _hookStart, 'UserPromptSubmit');
33
+ }
34
+ }
35
+ main().catch((e) => {
36
+ process.stderr.write(`[ch-hook] forge-loop-progress: ${e instanceof Error ? e.message : String(e)}\n`);
37
+ console.log(failOpenWithTracking('forge-loop-progress', e));
38
+ });
@@ -20,7 +20,7 @@ const require = createRequire(import.meta.url);
20
20
  * (Code Reflection + permission hints 주입 타이밍)
21
21
  * - 같은 이벤트 내 훅은 배열 순서대로 실행됨
22
22
  */
23
- import registryData from '../../hooks/hook-registry.json' with { type: 'json' };
23
+ import registryData from '../../assets/shared/hook-registry.json' with { type: 'json' };
24
24
  export const HOOK_REGISTRY = registryData;
25
25
  /** 티어별 훅 목록 조회 */
26
26
  export function getHooksByTier(tier) {
@@ -9,7 +9,7 @@
9
9
  * - forgen config hooks (사용자 설정 변경 후)
10
10
  * - forgen install (플러그인 설치 후)
11
11
  */
12
- import { type RuntimeHost } from '../core/types.js';
12
+ import type { RuntimeHost } from '../core/types.js';
13
13
  interface HookCommand {
14
14
  type: 'command';
15
15
  command: string;
@@ -30,6 +30,12 @@ interface GenerateOptions {
30
30
  pluginRoot?: string;
31
31
  /** 런타임 (claude|codex) */
32
32
  runtime?: RuntimeHost;
33
+ /**
34
+ * 환경 독립 산출물 모드 (W4, 2026-04-27).
35
+ * true 시 plugin 감지 + hook-config 비활성화 모두 건너뛰어 모든 hook 이 active.
36
+ * 배포(prepack), 테스트 결정론, runtime 환경 분리에 사용.
37
+ */
38
+ releaseMode?: boolean;
33
39
  }
34
40
  /**
35
41
  * 활성 훅만 포함한 hooks.json 객체를 생성합니다.
@@ -39,6 +45,14 @@ interface GenerateOptions {
39
45
  * 2. 충돌 훅 식별
40
46
  * 3. hook-config.json 설정 적용
41
47
  * 4. 활성 훅만 hooks.json 구조로 변환
48
+ *
49
+ * releaseMode: 환경 독립 산출물 모드 (W4, 2026-04-27).
50
+ * - true 시 plugin 감지를 건너뛰고, hook-config.json 의 사용자 비활성화도 무시한다.
51
+ * - 결과는 항상 모든 hook active — 배포 산출물 결정론화 + 테스트 안정화.
52
+ * - prepack-hooks.cjs 는 이미 HOME swap 으로 같은 효과를 내지만, 본 옵션은
53
+ * 명시적 API 로 동일 보장을 제공해 테스트가 환경 독립 검증 가능.
54
+ * - 자기증거: 본 세션이 사용자 HOME 에서 19/21 active 산출물을 받아 우회한
55
+ * 사례 — docs/issues/W4-W5-self-evidence.md 박제.
42
56
  */
43
57
  export declare function generateHooksJson(options?: GenerateOptions): HooksJson;
44
58
  /**
@@ -14,6 +14,7 @@ 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
+ import { getHostRuntime } from '../host/host-runtime.js';
17
18
  function splitCommand(raw) {
18
19
  const tokens = raw.match(/"([^"]+)"|\S+/g) ?? [];
19
20
  const unquoted = tokens.map(token => token.replace(/^"/, '').replace(/"$/, ''));
@@ -24,16 +25,9 @@ function quoteArg(raw) {
24
25
  }
25
26
  function buildHookCommand(pluginRoot, rawScript, runtime) {
26
27
  const { script, args } = splitCommand(rawScript);
27
- const scriptPath = `${pluginRoot}/${script}`;
28
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)}`;
29
+ // Phase 2: host-runtime 위임 — Codex 표면 (codex-adapter 경유) 을 core 가 모르도록.
30
+ return getHostRuntime(runtime).wrapHookCommand(pluginRoot, script, quotedArgs);
37
31
  }
38
32
  /**
39
33
  * 활성 훅만 포함한 hooks.json 객체를 생성합니다.
@@ -43,23 +37,31 @@ function buildHookCommand(pluginRoot, rawScript, runtime) {
43
37
  * 2. 충돌 훅 식별
44
38
  * 3. hook-config.json 설정 적용
45
39
  * 4. 활성 훅만 hooks.json 구조로 변환
40
+ *
41
+ * releaseMode: 환경 독립 산출물 모드 (W4, 2026-04-27).
42
+ * - true 시 plugin 감지를 건너뛰고, hook-config.json 의 사용자 비활성화도 무시한다.
43
+ * - 결과는 항상 모든 hook active — 배포 산출물 결정론화 + 테스트 안정화.
44
+ * - prepack-hooks.cjs 는 이미 HOME swap 으로 같은 효과를 내지만, 본 옵션은
45
+ * 명시적 API 로 동일 보장을 제공해 테스트가 환경 독립 검증 가능.
46
+ * - 자기증거: 본 세션이 사용자 HOME 에서 19/21 active 산출물을 받아 우회한
47
+ * 사례 — docs/issues/W4-W5-self-evidence.md 박제.
46
48
  */
47
49
  export function generateHooksJson(options) {
48
50
  const cwd = options?.cwd;
51
+ const releaseMode = options?.releaseMode ?? false;
49
52
  // biome-ignore lint/suspicious/noTemplateCurlyInString: CLAUDE_PLUGIN_ROOT is a Claude Code Plugin SDK variable resolved at runtime
50
53
  const pluginRoot = options?.pluginRoot ?? '${CLAUDE_PLUGIN_ROOT}/dist';
51
54
  const runtime = options?.runtime ?? 'claude';
52
- // 다른 플러그인의 충돌 훅 감지
53
- const hookConflicts = getHookConflicts(cwd);
54
- const detectedPlugins = detectInstalledPlugins(cwd);
55
- const hasOtherPlugins = detectedPlugins.length > 0;
55
+ // 다른 플러그인의 충돌 훅 감지 — releaseMode 시 건너뜀
56
+ const hookConflicts = releaseMode ? new Set() : getHookConflicts(cwd);
57
+ const hasOtherPlugins = !releaseMode && detectInstalledPlugins(cwd).length > 0;
56
58
  // 활성 훅 필터링
57
59
  const activeHooks = HOOK_REGISTRY.filter(hook => {
58
- // 1) hook-config.json에서 명시적 비활성화
59
- if (!isHookEnabled(hook.name))
60
+ // 1) hook-config.json에서 명시적 비활성화 (releaseMode 시 무시)
61
+ if (!releaseMode && !isHookEnabled(hook.name))
60
62
  return false;
61
63
  // 2) 다른 플러그인과 충돌하는 workflow 훅은 자동 비활성
62
- // (단, compound-critical 훅은 항상 유지)
64
+ // (단, compound-critical 훅은 항상 유지. releaseMode 면 분기 조건이 false)
63
65
  if (hasOtherPlugins && hook.tier === 'workflow' && hookConflicts.has(hook.name) && !hook.compoundCritical) {
64
66
  return false;
65
67
  }
@@ -83,5 +83,5 @@ async function main() {
83
83
  }
84
84
  main().catch((e) => {
85
85
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
86
- console.log(failOpenWithTracking('intent-classifier'));
86
+ console.log(failOpenWithTracking('intent-classifier', e));
87
87
  });
@@ -106,7 +106,7 @@ function loadSkillContent(skillName) {
106
106
  // 글로벌 스킬 경로
107
107
  searchPaths.push(path.join(FORGEN_HOME, 'skills', `${skillName}.md`));
108
108
  // forgen 패키지 내장 스킬
109
- const pkgSkillPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands', `${skillName}.md`);
109
+ const pkgSkillPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'claude', 'commands', `${skillName}.md`);
110
110
  searchPaths.push(pkgSkillPath);
111
111
  for (const p of searchPaths) {
112
112
  if (fs.existsSync(p)) {
@@ -308,6 +308,6 @@ async function main() {
308
308
  if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
309
309
  main().catch((e) => {
310
310
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
311
- console.log(failOpenWithTracking('keyword-detector'));
311
+ console.log(failOpenWithTracking('keyword-detector', e));
312
312
  });
313
313
  }
@@ -50,5 +50,5 @@ async function main() {
50
50
  }
51
51
  main().catch((e) => {
52
52
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
53
- console.log(failOpenWithTracking('notepad-injector'));
53
+ console.log(failOpenWithTracking('notepad-injector', e));
54
54
  });
@@ -129,5 +129,5 @@ async function main() {
129
129
  }
130
130
  main().catch((e) => {
131
131
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
132
- console.log(failOpenWithTracking('permission-handler'));
132
+ console.log(failOpenWithTracking('permission-handler', e));
133
133
  });
@@ -127,5 +127,5 @@ main().catch((e) => {
127
127
  hookName: 'post-tool-failure', eventType: 'PostToolUseFailure', cause: e,
128
128
  });
129
129
  process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
130
- console.log(failOpenWithTracking('post-tool-failure'));
130
+ console.log(failOpenWithTracking('post-tool-failure', e));
131
131
  });
@@ -18,6 +18,12 @@ interface ModifiedFilesState {
18
18
  recentWrites?: Record<string, string[]>;
19
19
  /** Drift detection state */
20
20
  drift?: DriftState;
21
+ /**
22
+ * TEST-2 support: 최근 N개 tool 이름 (가장 최근이 마지막). 세션 시작 이래 누적된
23
+ * 도구 이름을 그대로 끝까지 보관하면 메모리 낭비이므로 slice window.
24
+ * stop-guard 가 "측정 도구 호출 수" 를 빠르게 계산.
25
+ */
26
+ recentToolNames?: string[];
21
27
  }
22
28
  export declare const ERROR_PATTERNS: Array<{
23
29
  pattern: RegExp;
@@ -32,7 +38,7 @@ export interface AgentValidationResult {
32
38
  severity: 'info' | 'warning' | 'error';
33
39
  message: string;
34
40
  }
35
- export declare function validateAgentOutput(toolResponse: string): AgentValidationResult | null;
41
+ export declare function validateAgentOutput(toolResponse: unknown): AgentValidationResult | null;
36
42
  export declare function trackModifiedFile(state: ModifiedFilesState, filePath: string, toolName: string): {
37
43
  state: ModifiedFilesState;
38
44
  count: number;