@wooojin/forgen 0.4.1 → 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 (140) hide show
  1. package/.claude-plugin/plugin.json +5 -5
  2. package/CHANGELOG.md +164 -15
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.ja.md +17 -9
  5. package/README.ko.md +20 -12
  6. package/README.md +46 -12
  7. package/README.zh.md +17 -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/cli.js +78 -6
  24. package/dist/core/auto-compound-runner.js +62 -38
  25. package/dist/core/behavior-classifier.d.ts +28 -0
  26. package/dist/core/behavior-classifier.js +46 -0
  27. package/dist/core/dashboard.d.ts +7 -0
  28. package/dist/core/dashboard.js +32 -0
  29. package/dist/core/doctor.js +92 -0
  30. package/dist/core/git-stats.d.ts +36 -0
  31. package/dist/core/git-stats.js +79 -0
  32. package/dist/core/harness.d.ts +1 -1
  33. package/dist/core/harness.js +27 -20
  34. package/dist/core/host-detect.d.ts +42 -0
  35. package/dist/core/host-detect.js +68 -0
  36. package/dist/core/installer.js +2 -2
  37. package/dist/core/migrate-cli.d.ts +1 -0
  38. package/dist/core/migrate-cli.js +19 -0
  39. package/dist/core/migrate-evidence-host.d.ts +36 -0
  40. package/dist/core/migrate-evidence-host.js +49 -0
  41. package/dist/core/settings-injector.js +4 -2
  42. package/dist/core/spawn.d.ts +1 -1
  43. package/dist/core/spawn.js +4 -11
  44. package/dist/core/stats-cli.js +12 -0
  45. package/dist/core/trust-layer-intent.d.ts +35 -0
  46. package/dist/core/trust-layer-intent.js +30 -0
  47. package/dist/core/types.d.ts +1 -1
  48. package/dist/engine/compound-extractor.js +7 -9
  49. package/dist/engine/learn-cli.js +4 -2
  50. package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
  51. package/dist/engine/lifecycle/bypass-detector.js +57 -5
  52. package/dist/fgx.js +2 -1
  53. package/dist/forge/evidence-processor.js +12 -0
  54. package/dist/forge/onboarding.d.ts +3 -2
  55. package/dist/forge/onboarding.js +3 -2
  56. package/dist/hooks/db-guard.js +3 -3
  57. package/dist/hooks/forge-loop-progress.d.ts +9 -0
  58. package/dist/hooks/forge-loop-progress.js +38 -0
  59. package/dist/hooks/hook-registry.js +1 -1
  60. package/dist/hooks/hooks-generator.d.ts +15 -1
  61. package/dist/hooks/hooks-generator.js +18 -16
  62. package/dist/hooks/keyword-detector.js +1 -1
  63. package/dist/hooks/post-tool-use.d.ts +1 -1
  64. package/dist/hooks/post-tool-use.js +13 -4
  65. package/dist/hooks/pre-compact.js +1 -1
  66. package/dist/hooks/pre-tool-use.js +4 -4
  67. package/dist/hooks/rate-limiter.js +2 -2
  68. package/dist/hooks/session-recovery.js +11 -0
  69. package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
  70. package/dist/hooks/shared/blocking-allowlist.js +38 -0
  71. package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
  72. package/dist/hooks/shared/forge-loop-state.js +116 -0
  73. package/dist/hooks/shared/hook-response.d.ts +18 -0
  74. package/dist/hooks/shared/hook-response.js +31 -0
  75. package/dist/hooks/skill-injector.js +1 -1
  76. package/dist/hooks/stop-guard.js +15 -0
  77. package/dist/host/capabilities-claude.d.ts +8 -0
  78. package/dist/host/capabilities-claude.js +46 -0
  79. package/dist/host/capabilities-codex.d.ts +11 -0
  80. package/dist/host/capabilities-codex.js +50 -0
  81. package/dist/host/capabilities-registry.d.ts +11 -0
  82. package/dist/host/capabilities-registry.js +30 -0
  83. package/dist/host/codex-adapter.d.ts +8 -5
  84. package/dist/host/codex-adapter.js +10 -82
  85. package/dist/host/codex-output-parser.d.ts +39 -0
  86. package/dist/host/codex-output-parser.js +75 -0
  87. package/dist/host/exec-host.d.ts +54 -0
  88. package/dist/host/exec-host.js +92 -0
  89. package/dist/host/host-runtime.d.ts +37 -0
  90. package/dist/host/host-runtime.js +51 -0
  91. package/dist/host/install-claude.d.ts +35 -0
  92. package/dist/host/install-claude.js +238 -0
  93. package/dist/host/install-codex.d.ts +44 -0
  94. package/dist/host/install-codex.js +276 -0
  95. package/dist/host/install-orchestrator.d.ts +34 -0
  96. package/dist/host/install-orchestrator.js +126 -0
  97. package/dist/host/invoke-agent.d.ts +27 -0
  98. package/dist/host/invoke-agent.js +115 -0
  99. package/dist/host/parity-harness.d.ts +62 -0
  100. package/dist/host/parity-harness.js +283 -0
  101. package/dist/host/projection.d.ts +35 -0
  102. package/dist/host/projection.js +126 -0
  103. package/dist/mcp/server.js +11 -0
  104. package/dist/mcp/tools.js +47 -0
  105. package/dist/services/session.d.ts +6 -3
  106. package/dist/services/session.js +33 -4
  107. package/dist/store/evidence-store.d.ts +1 -0
  108. package/dist/store/evidence-store.js +34 -3
  109. package/dist/store/host-mismatch.d.ts +42 -0
  110. package/dist/store/host-mismatch.js +65 -0
  111. package/dist/store/profile-store.d.ts +29 -0
  112. package/dist/store/profile-store.js +53 -0
  113. package/dist/store/types.d.ts +13 -0
  114. package/hooks/hooks.json +6 -1
  115. package/package.json +6 -4
  116. package/plugin.json +4 -4
  117. package/scripts/postinstall.js +100 -25
  118. /package/{agents → assets/claude/agents}/analyst.md +0 -0
  119. /package/{agents → assets/claude/agents}/architect.md +0 -0
  120. /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
  121. /package/{agents → assets/claude/agents}/critic.md +0 -0
  122. /package/{agents → assets/claude/agents}/debugger.md +0 -0
  123. /package/{agents → assets/claude/agents}/designer.md +0 -0
  124. /package/{agents → assets/claude/agents}/executor.md +0 -0
  125. /package/{agents → assets/claude/agents}/explore.md +0 -0
  126. /package/{agents → assets/claude/agents}/git-master.md +0 -0
  127. /package/{agents → assets/claude/agents}/planner.md +0 -0
  128. /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
  129. /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
  130. /package/{agents → assets/claude/agents}/verifier.md +0 -0
  131. /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
  132. /package/{commands → assets/claude/commands}/calibrate.md +0 -0
  133. /package/{commands → assets/claude/commands}/code-review.md +0 -0
  134. /package/{commands → assets/claude/commands}/compound.md +0 -0
  135. /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
  136. /package/{commands → assets/claude/commands}/docker.md +0 -0
  137. /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
  138. /package/{commands → assets/claude/commands}/learn.md +0 -0
  139. /package/{commands → assets/claude/commands}/retro.md +0 -0
  140. /package/{commands → assets/claude/commands}/ship.md +0 -0
@@ -4,12 +4,17 @@
4
4
  * Rule.policy 자연어에서 "피해야 할 패턴" 을 추출하고, Write/Edit/Bash 도구
5
5
  * 출력에서 해당 패턴을 찾아 BypassEntry 후보로 반환한다.
6
6
  *
7
- * Heuristic:
7
+ * Heuristic priority (most explicit first):
8
+ * 0) Parenthesized examples (e.g., "(rm -rf, DROP, force-push)") → tokens inside
8
9
  * 1) "use X not Y" / "use X instead of Y" / "X over Y" → bypass = Y
9
10
  * 2) "avoid X" / "don't use X" / "never use X" / "do not use X" → bypass = X
10
11
  * 3) Korean: "X 말라" / "X 금지" / "X 하지 않" → bypass = X
11
12
  * 4) 그 외: 빈 배열 (탐지 불가).
12
13
  *
14
+ * Stop list filter: generic Korean verbs (실행/사용/선언/...) extracted by Korean
15
+ * heuristic are removed — they cause massive FP (RC5/E9: matched the word "실행"
16
+ * everywhere instead of "rm -rf"). 64 false-positive bypasses observed before fix.
17
+ *
13
18
  * 반환된 패턴은 escape 된 정규식 문자열 — caller 가 `new RegExp(p)` 로 사용.
14
19
  */
15
20
  function escapeRegex(s) {
@@ -29,9 +34,50 @@ function trimPunct(s) {
29
34
  out = out.replace(/^[,;:!?"'`(]+|[.,;:!?"'`)]+$/g, '');
30
35
  return out;
31
36
  }
37
+ /**
38
+ * Generic Korean verbs/words that produce massive false positives if used as
39
+ * bypass patterns (RC5/E9 fix). Extending requires retro evidence.
40
+ */
41
+ const KO_GENERIC_STOP_WORDS = new Set([
42
+ '실행', '사용', '선언', '수행', '처리', '작성', '호출', '적용',
43
+ '실행하지', '사용하지', '선언하지', '수행하지', '처리하지',
44
+ // English fallthroughs (already low value as bypass signals)
45
+ 'use', 'do', 'execute',
46
+ ]);
47
+ /** Korean markers that signal the parenthesized content is NOT an example list. */
48
+ const KO_NON_EXAMPLE_MARKERS = ['제외', '한정', '예외', '단서', 'except'];
49
+ /** Extract concrete tokens inside parenthesized example list. */
50
+ function extractParenthesizedExamples(p) {
51
+ const out = [];
52
+ // Match (...) groups; multiple groups in policy are uncommon but supported
53
+ const re = /\(([^)]+)\)/g;
54
+ let m;
55
+ while ((m = re.exec(p))) {
56
+ const inside = m[1];
57
+ // Skip if it looks like a path (contains "/" before any obvious separator commitment)
58
+ if (/[a-zA-Z]+\/[a-zA-Z]/.test(inside))
59
+ continue;
60
+ // Skip if it's an exclusion / scope-restriction note (Korean markers)
61
+ if (KO_NON_EXAMPLE_MARKERS.some((mk) => inside.includes(mk)))
62
+ continue;
63
+ // Skip if any single segment is suspiciously long (full sentence rather than token)
64
+ const segs = inside.split(/[,]|\s+(?:or|와|및)\s+/i).map((s) => s.trim());
65
+ if (segs.some((s) => s.length > 30))
66
+ continue;
67
+ const tokens = segs
68
+ .map((t) => trimPunct(t))
69
+ .filter((t) => t.length >= 2 && !KO_GENERIC_STOP_WORDS.has(t));
70
+ out.push(...tokens);
71
+ }
72
+ return out;
73
+ }
32
74
  export function extractBypassPatterns(rule) {
33
75
  const patterns = [];
34
76
  const p = rule.policy;
77
+ // 0) Parenthesized examples (highest priority — explicit signal)
78
+ for (const ex of extractParenthesizedExamples(p)) {
79
+ patterns.push(escapeRegex(ex));
80
+ }
35
81
  // use X not Y / use X instead of Y / use X over Y
36
82
  // X, Y may contain dots (e.g., ".then()", "vi.mock"). Strip trailing punctuation.
37
83
  const useNot = p.match(/\b(?:use|prefer|choose)\s+(\S+?)\s+(?:not|instead\s+of|over|rather\s+than)\s+(\S+)/i);
@@ -43,10 +89,16 @@ export function extractBypassPatterns(rule) {
43
89
  patterns.push(escapeRegex(trimPunct(avoid[1])));
44
90
  // Korean: "X 말라" / "X 금지" / "X 하지 마"
45
91
  const ko = p.match(/(\S+)\s*(?:말라|금지|하지\s*마|쓰지\s*마)/);
46
- if (ko)
47
- patterns.push(escapeRegex(trimPunct(ko[1])));
48
- // Dedupe + filter trivial
49
- return [...new Set(patterns)].filter((pat) => pat.length >= 2);
92
+ if (ko) {
93
+ const candidate = trimPunct(ko[1]);
94
+ if (!KO_GENERIC_STOP_WORDS.has(candidate)) {
95
+ patterns.push(escapeRegex(candidate));
96
+ }
97
+ }
98
+ // Dedupe + filter trivial + filter stop-words (defense in depth)
99
+ return [...new Set(patterns)]
100
+ .filter((pat) => pat.length >= 2)
101
+ .filter((pat) => !KO_GENERIC_STOP_WORDS.has(pat.replace(/\\/g, '')));
50
102
  }
51
103
  /**
52
104
  * Pure — rules + tool output 으로 bypass candidates 추출.
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: 애매한 구현 요청 + 인접 영향 가능성
@@ -9,7 +9,7 @@ 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
14
  import { preprocessForMatch } from './shared/command-parser.js';
15
15
  const FAIL_COUNTER_PATH = path.join(STATE_DIR, 'db-guard-fail-counter.json');
@@ -89,7 +89,7 @@ async function main() {
89
89
  if (!data) {
90
90
  const failCount = getAndIncrementFailCount();
91
91
  if (failCount >= FAIL_CLOSE_THRESHOLD) {
92
- 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.`));
93
93
  }
94
94
  else {
95
95
  process.stderr.write(`[ch-hook] db-guard stdin parse failed (${failCount}/${FAIL_CLOSE_THRESHOLD})\n`);
@@ -106,7 +106,7 @@ async function main() {
106
106
  const toolInput = data.tool_input ?? data.toolInput ?? {};
107
107
  const check = checkDangerousSql(toolName, toolInput);
108
108
  if (check.action === 'block') {
109
- console.log(deny(`[Forgen] Dangerous SQL blocked: ${check.description}`));
109
+ console.log(denyOrObserve('db-guard', `[Forgen] Dangerous SQL blocked: ${check.description}`));
110
110
  return;
111
111
  }
112
112
  if (check.action === 'warn') {
@@ -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
  }
@@ -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)) {
@@ -38,7 +38,7 @@ export interface AgentValidationResult {
38
38
  severity: 'info' | 'warning' | 'error';
39
39
  message: string;
40
40
  }
41
- export declare function validateAgentOutput(toolResponse: string): AgentValidationResult | null;
41
+ export declare function validateAgentOutput(toolResponse: unknown): AgentValidationResult | null;
42
42
  export declare function trackModifiedFile(state: ModifiedFilesState, filePath: string, toolName: string): {
43
43
  state: ModifiedFilesState;
44
44
  count: number;
@@ -75,15 +75,21 @@ const AGENT_QUALITY_PATTERNS = [
75
75
  { pattern: /(?:context (?:window|limit) (?:exceeded|reached)|too (?:large|long) to (?:read|process))/i, signal: 'agent_context_overflow', severity: 'warning', message: 'Agent hit context limits — output may be incomplete' },
76
76
  ];
77
77
  export function validateAgentOutput(toolResponse) {
78
- if (!toolResponse || toolResponse.trim().length < AGENT_MIN_OUTPUT_LENGTH) {
78
+ // tool_response string / object / array 모두 가능. main() 측에서 stringify 를 한 번 더
79
+ // 하지만 직접 호출 보호 (defense in depth).
80
+ if (typeof toolResponse !== 'string') {
81
+ toolResponse = toolResponse == null ? '' : JSON.stringify(toolResponse);
82
+ }
83
+ const r = toolResponse;
84
+ if (!r || r.trim().length < AGENT_MIN_OUTPUT_LENGTH) {
79
85
  return {
80
86
  signal: 'agent_empty_output',
81
87
  severity: 'warning',
82
- message: `Agent returned minimal output (${toolResponse?.trim().length ?? 0} chars). Verify the result is usable.`,
88
+ message: `Agent returned minimal output (${r.trim().length} chars). Verify the result is usable.`,
83
89
  };
84
90
  }
85
91
  for (const p of AGENT_QUALITY_PATTERNS) {
86
- if (p.pattern.test(toolResponse)) {
92
+ if (p.pattern.test(r)) {
87
93
  return { signal: p.signal, severity: p.severity, message: p.message };
88
94
  }
89
95
  }
@@ -114,7 +120,10 @@ async function main() {
114
120
  }
115
121
  const toolName = data.tool_name ?? data.toolName ?? '';
116
122
  const toolInput = data.tool_input ?? data.toolInput ?? {};
117
- const toolResponse = data.tool_response ?? data.toolOutput ?? '';
123
+ // tool_response string / object / array 모두 가능 (sub-agent 결과는 object 가 흔함).
124
+ // 모든 downstream 이 string 가정이라 stringify 로 normalize. 회귀 박제: tests/hooks/post-tool-use.test.ts
125
+ const rawResponse = data.tool_response ?? data.toolOutput ?? '';
126
+ const toolResponse = typeof rawResponse === 'string' ? rawResponse : JSON.stringify(rawResponse);
118
127
  const sessionId = data.session_id ?? 'default';
119
128
  const modState = loadModifiedFiles(sessionId);
120
129
  modState.toolCallCount = (modState.toolCallCount ?? 0) + 1;
@@ -74,7 +74,7 @@ export function buildSessionBrief(sessionId) {
74
74
  }
75
75
  catch { /* fail-open */ }
76
76
  // solutionsInjected: read injection-cache-*.json files, collect solutions[].name
77
- let solutionsInjected = [];
77
+ const solutionsInjected = [];
78
78
  try {
79
79
  if (fs.existsSync(STATE_DIR)) {
80
80
  for (const f of fs.readdirSync(STATE_DIR)) {
@@ -19,7 +19,7 @@ import { sanitizeId } from './shared/sanitize-id.js';
19
19
  import { incrementEvidence } from '../engine/solution-writer.js';
20
20
  import { isReflectionCandidate } from './compound-reflection.js';
21
21
  import { isHookEnabled } from './hook-config.js';
22
- import { approve, approveWithWarning, deny, failOpenWithTracking } from './shared/hook-response.js';
22
+ import { approve, approveWithWarning, denyOrObserve, failOpenWithTracking } from './shared/hook-response.js';
23
23
  import { FORGEN_HOME, STATE_DIR } from '../core/paths.js';
24
24
  import { recordHookTiming } from './shared/hook-timing.js';
25
25
  import { maskQuotedContent } from './shared/command-parser.js';
@@ -305,7 +305,7 @@ async function main() {
305
305
  // for `forgen doctor` / log inspection. Mirrors `db-guard.ts:85-96`.
306
306
  const failCount = getAndIncrementFailCount();
307
307
  if (failCount >= FAIL_CLOSE_THRESHOLD) {
308
- console.log(deny(`[Forgen] PreToolUse: stdin parse failed ${failCount} consecutive times — blocking for safety.`));
308
+ console.log(denyOrObserve('pre-tool-use', `[Forgen] PreToolUse: stdin parse failed ${failCount} consecutive times — blocking for safety.`));
309
309
  }
310
310
  else {
311
311
  process.stderr.write(`[ch-hook] pre-tool-use stdin parse failed (${failCount}/${FAIL_CLOSE_THRESHOLD})\n`);
@@ -366,7 +366,7 @@ async function main() {
366
366
  const baseMsg = spec.block_message ?? `[${rule.rule_id}] policy violation: ${rule.policy.slice(0, 120)}`;
367
367
  // G8: override 힌트 — FORGEN_USER_CONFIRMED=1 으로 사용자 명시 승인 가능, 감사 로그 기록됨.
368
368
  const msgWithHint = `${baseMsg}\n\n(override: set FORGEN_USER_CONFIRMED=1 (bypass will be audited in violations.jsonl))`;
369
- console.log(deny(msgWithHint));
369
+ console.log(denyOrObserve('pre-tool-use', msgWithHint));
370
370
  return;
371
371
  }
372
372
  if (requiresFlag && confirmed) {
@@ -388,7 +388,7 @@ async function main() {
388
388
  // Bash 도구: 위험 명령어 감지 (빌트인 safety net)
389
389
  const check = checkDangerousCommand(toolName, toolInput);
390
390
  if (check.action === 'block') {
391
- console.log(deny(`[Forgen] Dangerous command blocked: ${check.description}\nCommand: ${check.command}`));
391
+ console.log(denyOrObserve('pre-tool-use', `[Forgen] Dangerous command blocked: ${check.description}\nCommand: ${check.command}`));
392
392
  return;
393
393
  }
394
394
  if (check.action === 'warn') {
@@ -10,7 +10,7 @@ import * as path from 'node:path';
10
10
  import { readStdinJSON } from './shared/read-stdin.js';
11
11
  import { atomicWriteJSON } from './shared/atomic-write.js';
12
12
  import { isHookEnabled } from './hook-config.js';
13
- import { approve, deny, failOpenWithTracking } from './shared/hook-response.js';
13
+ import { approve, denyOrObserve, failOpenWithTracking } from './shared/hook-response.js';
14
14
  import { STATE_DIR } from '../core/paths.js';
15
15
  const RATE_LIMIT_PATH = path.join(STATE_DIR, 'rate-limit.json');
16
16
  const DEFAULT_LIMIT = 30; // calls per minute
@@ -75,7 +75,7 @@ async function main() {
75
75
  saveRateLimitState(updatedState);
76
76
  }
77
77
  if (exceeded) {
78
- console.log(deny(`[Forgen] Rate limit exceeded (${count}/${DEFAULT_LIMIT}/min). Wait before retrying.`));
78
+ console.log(denyOrObserve('rate-limiter', `[Forgen] Rate limit exceeded (${count}/${DEFAULT_LIMIT}/min). Wait before retrying.`));
79
79
  return;
80
80
  }
81
81
  console.log(approve());
@@ -320,6 +320,17 @@ async function main() {
320
320
  }
321
321
  }
322
322
  catch { /* fail-open */ }
323
+ // US-M1 (RC6 가드): 직전 forge-loop findings 또는 진행 중 stories 자동 inject.
324
+ // 본 세션 자기증거 — head -80 truncation 으로 findings 누락 → 같은 가설 재발.
325
+ try {
326
+ const { readForgeLoopState, renderForgeLoopForSession } = await import('./shared/forge-loop-state.js');
327
+ const block = renderForgeLoopForSession(readForgeLoopState());
328
+ if (block)
329
+ recoveryMessages.push(block);
330
+ }
331
+ catch (e) {
332
+ log.debug('forge-loop findings inject 실패', e);
333
+ }
323
334
  const sessionId = sessionContext.sessionId;
324
335
  // 이전 세션 자동 compound (fire-and-forget)
325
336
  // /new로 세션 리셋 시 SessionStart가 다시 호출됨 — 이때 이전 transcript를 compound
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Blocking ALLOW-LIST — P3' (2026-04-27)
3
+ *
4
+ * 사용자 작업을 차단(block)할 권한을 가진 hook 의 명시적 화이트리스트.
5
+ * 목록 외 hook 의 부정적 판정은 "관찰 신호"(log only) 로만 처리되어야 한다.
6
+ *
7
+ * RC5 (retro-v040): 분산된 detector 가 각자 block 결정을 내리면서 false-positive
8
+ * 가 메인 로직 흐름까지 차단하는 회귀 패턴 발생. ALLOW-LIST 명시화로 차단 권한
9
+ * 의 source-of-truth 를 단일화.
10
+ *
11
+ * v0.4.2 정책:
12
+ * - 본 모듈은 ALLOW-LIST 정의 + 검증 helper. 기존 deny() 직접 호출 hook 들은
13
+ * v0.4.2 에서 denyOrObserve(name, reason) 로 마이그레이션 완료.
14
+ * - 신규 hook 추가 시 차단 권한이 필요하면 본 ALLOW-LIST 에 추가 + 본 파일의
15
+ * 사유 문서화 의무. 본 commit diff 가 review 필수 항목.
16
+ *
17
+ * 멤버 사유:
18
+ * - stop-guard: Stop hook — false-completion 메타 가드 (자가 검증 강제)
19
+ * - pre-tool-use: Bash dangerous-pattern + 수동 confirm 가드
20
+ * - secret-filter: Write/Edit 결과의 .env / API key 노출 차단
21
+ * - db-guard: Bash 의 destructive DB 명령 (DROP/TRUNCATE/DELETE) 차단
22
+ * - rate-limiter: 사용자 작업 빈도 임계 초과 시 cool-down 차단 (resource abuse 방어)
23
+ */
24
+ export declare const BLOCKING_ALLOWLIST: ReadonlySet<string>;
25
+ /** hook 이 block 결정을 출력할 권한이 있는지. */
26
+ export declare function canBlock(hookName: string): boolean;
27
+ /** ALLOW-LIST 에 추가하려는 hook 이 정책 문서화를 요구하는지 (lint helper). */
28
+ export declare function requiresPolicyDoc(hookName: string): boolean;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Blocking ALLOW-LIST — P3' (2026-04-27)
3
+ *
4
+ * 사용자 작업을 차단(block)할 권한을 가진 hook 의 명시적 화이트리스트.
5
+ * 목록 외 hook 의 부정적 판정은 "관찰 신호"(log only) 로만 처리되어야 한다.
6
+ *
7
+ * RC5 (retro-v040): 분산된 detector 가 각자 block 결정을 내리면서 false-positive
8
+ * 가 메인 로직 흐름까지 차단하는 회귀 패턴 발생. ALLOW-LIST 명시화로 차단 권한
9
+ * 의 source-of-truth 를 단일화.
10
+ *
11
+ * v0.4.2 정책:
12
+ * - 본 모듈은 ALLOW-LIST 정의 + 검증 helper. 기존 deny() 직접 호출 hook 들은
13
+ * v0.4.2 에서 denyOrObserve(name, reason) 로 마이그레이션 완료.
14
+ * - 신규 hook 추가 시 차단 권한이 필요하면 본 ALLOW-LIST 에 추가 + 본 파일의
15
+ * 사유 문서화 의무. 본 commit diff 가 review 필수 항목.
16
+ *
17
+ * 멤버 사유:
18
+ * - stop-guard: Stop hook — false-completion 메타 가드 (자가 검증 강제)
19
+ * - pre-tool-use: Bash dangerous-pattern + 수동 confirm 가드
20
+ * - secret-filter: Write/Edit 결과의 .env / API key 노출 차단
21
+ * - db-guard: Bash 의 destructive DB 명령 (DROP/TRUNCATE/DELETE) 차단
22
+ * - rate-limiter: 사용자 작업 빈도 임계 초과 시 cool-down 차단 (resource abuse 방어)
23
+ */
24
+ export const BLOCKING_ALLOWLIST = new Set([
25
+ 'stop-guard',
26
+ 'pre-tool-use',
27
+ 'secret-filter',
28
+ 'db-guard',
29
+ 'rate-limiter',
30
+ ]);
31
+ /** hook 이 block 결정을 출력할 권한이 있는지. */
32
+ export function canBlock(hookName) {
33
+ return BLOCKING_ALLOWLIST.has(hookName);
34
+ }
35
+ /** ALLOW-LIST 에 추가하려는 hook 이 정책 문서화를 요구하는지 (lint helper). */
36
+ export function requiresPolicyDoc(hookName) {
37
+ return !BLOCKING_ALLOWLIST.has(hookName);
38
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Forge Loop State — RC6 가드 (US-M1)
3
+ *
4
+ * 직전 forge-loop 의 findings 또는 진행 중 stories 를 ≤1KB 요약으로 렌더한다.
5
+ * SessionStart 와 UserPromptSubmit 두 hook 이 공유하는 단일 진입점.
6
+ *
7
+ * RC6 자기증거: 본 세션 R1 에서 head -80 으로 forge-loop.json 을 읽어 findings
8
+ * 8줄(line 92~99)이 잘렸음. 결과적으로 직전 결론을 컨텍스트에 못 가져 같은
9
+ * 가설을 재발. 이 모듈은 그 회귀를 시스템 레벨에서 차단한다.
10
+ */
11
+ interface ForgeLoopStory {
12
+ id: string;
13
+ title: string;
14
+ passes?: boolean;
15
+ }
16
+ export interface ForgeLoopState {
17
+ active?: boolean;
18
+ task?: string;
19
+ startedAt?: string;
20
+ completedAt?: string;
21
+ stories?: ForgeLoopStory[];
22
+ findings?: Record<string, string>;
23
+ }
24
+ export declare function readForgeLoopState(filePath?: string): ForgeLoopState | null;
25
+ /** SessionStart 용 — 완료된 forge-loop 의 findings 또는 진행 중 stories 요약. */
26
+ export declare function renderForgeLoopForSession(state: ForgeLoopState | null, now?: number): string | null;
27
+ /** UserPromptSubmit 용 — active=true 시에만 짧은 진행 상황 1~2줄. */
28
+ export declare function renderForgeLoopForPrompt(state: ForgeLoopState | null, now?: number): string | null;
29
+ /** 테스트 노출용 상수 — 회귀 시 임계값 변경 즉시 감지. */
30
+ export declare const FORGE_LOOP_LIMITS: {
31
+ readonly SOFT_STALE_MS: number;
32
+ readonly HARD_STALE_MS: number;
33
+ readonly MAX_INJECT_BYTES: 1024;
34
+ readonly MAX_PENDING: 5;
35
+ };
36
+ export {};