@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.
- package/.claude-plugin/plugin.json +5 -5
- package/CHANGELOG.md +164 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +17 -9
- package/README.ko.md +20 -12
- package/README.md +46 -12
- package/README.zh.md +17 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- package/dist/cli.js +78 -6
- package/dist/core/auto-compound-runner.js +62 -38
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +32 -0
- package/dist/core/doctor.js +92 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -0
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +1 -0
- package/dist/core/migrate-cli.js +19 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.js +12 -0
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +4 -2
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/db-guard.js +3 -3
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +1 -1
- package/dist/hooks/post-tool-use.js +13 -4
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.js +4 -4
- package/dist/hooks/rate-limiter.js +2 -2
- package/dist/hooks/session-recovery.js +11 -0
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +18 -0
- package/dist/hooks/shared/hook-response.js +31 -0
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/stop-guard.js +15 -0
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +34 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +6 -4
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/calibrate.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/retro.md +0 -0
- /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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
*
|
|
5
|
-
* Authoritative spec: docs/
|
|
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';
|
package/dist/forge/onboarding.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forgen v1 — Onboarding
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Authoritative spec: docs/
|
|
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: 애매한 구현 요청 + 인접 영향 가능성
|
package/dist/hooks/db-guard.js
CHANGED
|
@@ -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,
|
|
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(
|
|
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(
|
|
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 '../../
|
|
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 {
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 (${
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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 {};
|