@wooojin/forgen 0.4.9 → 0.4.11
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 +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +33 -1
- package/assets/claude/agents/forgen-verify.md +65 -0
- package/assets/claude/workflows/compound-extract.js +136 -0
- package/assets/claude/workflows/evidence-gate-audit.js +107 -0
- package/assets/shared/hook-registry.json +1 -0
- package/dist/checks/_shared/meta-guard-dispatch.d.ts +38 -0
- package/dist/checks/_shared/meta-guard-dispatch.js +80 -0
- package/dist/checks/_shared/text-sanitizer.js +15 -0
- package/dist/cli.js +65 -2
- package/dist/core/changelog-cli.d.ts +7 -0
- package/dist/core/changelog-cli.js +100 -0
- package/dist/core/doctor.d.ts +3 -0
- package/dist/core/doctor.js +75 -1
- package/dist/core/effort-advisory.d.ts +23 -0
- package/dist/core/effort-advisory.js +29 -0
- package/dist/core/explain-cli.d.ts +6 -0
- package/dist/core/explain-cli.js +99 -0
- package/dist/core/git-stats.d.ts +23 -0
- package/dist/core/git-stats.js +50 -0
- package/dist/core/health-cli.d.ts +23 -0
- package/dist/core/health-cli.js +86 -0
- package/dist/core/inspect-cli.js +60 -1
- package/dist/core/probe-workflow-cli.d.ts +72 -0
- package/dist/core/probe-workflow-cli.js +282 -0
- package/dist/core/regress-map-cli.d.ts +7 -0
- package/dist/core/regress-map-cli.js +59 -0
- package/dist/core/stats-cli.d.ts +22 -9
- package/dist/core/stats-cli.js +149 -0
- package/dist/core/watch-cli.d.ts +7 -0
- package/dist/core/watch-cli.js +185 -0
- package/dist/core/workflows-cli.d.ts +26 -0
- package/dist/core/workflows-cli.js +120 -0
- package/dist/engine/compound-export.d.ts +12 -0
- package/dist/engine/compound-export.js +136 -14
- package/dist/engine/compound-extractor.d.ts +12 -43
- package/dist/engine/compound-extractor.js +27 -756
- package/dist/engine/extraction-diff.d.ts +11 -0
- package/dist/engine/extraction-diff.js +105 -0
- package/dist/engine/extraction-gates.d.ts +37 -0
- package/dist/engine/extraction-gates.js +100 -0
- package/dist/engine/extraction-git.d.ts +20 -0
- package/dist/engine/extraction-git.js +75 -0
- package/dist/engine/extraction-persistence.d.ts +27 -0
- package/dist/engine/extraction-persistence.js +140 -0
- package/dist/engine/extraction-session.d.ts +26 -0
- package/dist/engine/extraction-session.js +230 -0
- package/dist/engine/lifecycle/types.d.ts +1 -1
- package/dist/engine/meta-learning/matcher-weight-loader.d.ts +16 -0
- package/dist/engine/meta-learning/matcher-weight-loader.js +45 -0
- package/dist/engine/precision-guards.d.ts +14 -0
- package/dist/engine/precision-guards.js +39 -0
- package/dist/engine/ranking-pipeline.d.ts +45 -0
- package/dist/engine/ranking-pipeline.js +66 -0
- package/dist/engine/relevance-scorer.d.ts +43 -0
- package/dist/engine/relevance-scorer.js +81 -0
- package/dist/engine/scoring-algorithms.d.ts +31 -0
- package/dist/engine/scoring-algorithms.js +109 -0
- package/dist/engine/solution-matcher-eval.d.ts +97 -0
- package/dist/engine/solution-matcher-eval.js +122 -0
- package/dist/engine/solution-matcher.d.ts +21 -380
- package/dist/engine/solution-matcher.js +27 -828
- package/dist/fgx.d.ts +4 -1
- package/dist/fgx.js +46 -10
- package/dist/hooks/notepad-injector.js +7 -0
- package/dist/hooks/post-tool-use.js +8 -1
- package/dist/hooks/shared/preflight-check.d.ts +15 -0
- package/dist/hooks/shared/preflight-check.js +51 -0
- package/dist/hooks/stop-guard.js +19 -60
- package/dist/hooks/subagent-stop-guard.d.ts +23 -0
- package/dist/hooks/subagent-stop-guard.js +158 -0
- package/dist/hooks/subagent-tracker.d.ts +36 -3
- package/dist/hooks/subagent-tracker.js +86 -39
- package/dist/store/evidence-store.js +9 -2
- package/hooks/hooks.json +6 -1
- package/package.json +7 -7
- package/plugin.json +1 -1
- package/scripts/postinstall.js +10 -7
package/dist/cli.js
CHANGED
|
@@ -279,12 +279,13 @@ const commands = [
|
|
|
279
279
|
},
|
|
280
280
|
{
|
|
281
281
|
name: 'doctor',
|
|
282
|
-
description: 'Diagnostics (--prune-state to GC
|
|
282
|
+
description: 'Diagnostics (--quick for fast check, --prune-state to GC, --repair to auto-fix)',
|
|
283
283
|
handler: async (args) => {
|
|
284
284
|
const { runDoctor } = await import('./core/doctor.js');
|
|
285
285
|
await runDoctor({
|
|
286
286
|
pruneState: args.includes('--prune-state'),
|
|
287
287
|
repair: args.includes('--repair'),
|
|
288
|
+
quick: args.includes('--quick'),
|
|
288
289
|
});
|
|
289
290
|
},
|
|
290
291
|
},
|
|
@@ -341,6 +342,54 @@ const commands = [
|
|
|
341
342
|
await handleStats(args);
|
|
342
343
|
},
|
|
343
344
|
},
|
|
345
|
+
{
|
|
346
|
+
name: 'watch',
|
|
347
|
+
description: 'Real-time hook event stream (hook firings, blocks, solution matches)',
|
|
348
|
+
handler: async () => {
|
|
349
|
+
const { handleWatch } = await import('./core/watch-cli.js');
|
|
350
|
+
await handleWatch();
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: 'health',
|
|
355
|
+
description: 'Single-line health score (0-100) combining utilization, effectiveness, growth.',
|
|
356
|
+
handler: async () => {
|
|
357
|
+
const { handleHealth } = await import('./core/health-cli.js');
|
|
358
|
+
await handleHealth();
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'probe-workflow',
|
|
363
|
+
description: 'ADR-009 §1: measure whether dynamic-workflow subagents fire forgen hooks (arm|report|status).',
|
|
364
|
+
handler: async (args) => {
|
|
365
|
+
const { handleProbeWorkflow } = await import('./core/probe-workflow-cli.js');
|
|
366
|
+
await handleProbeWorkflow(args);
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'workflows',
|
|
371
|
+
description: 'Install/list forgen dynamic-workflow templates (install [--project] | list).',
|
|
372
|
+
handler: async (args) => {
|
|
373
|
+
const { handleWorkflows } = await import('./core/workflows-cli.js');
|
|
374
|
+
await handleWorkflows(args);
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'explain',
|
|
379
|
+
description: 'Explain the most recent block — what rule, why, and how to resolve.',
|
|
380
|
+
handler: async (args) => {
|
|
381
|
+
const { handleExplain } = await import('./core/explain-cli.js');
|
|
382
|
+
await handleExplain(args);
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: 'changelog',
|
|
387
|
+
description: 'Auto-summarize changes since last release tag (conventional commits).',
|
|
388
|
+
handler: async () => {
|
|
389
|
+
const { handleChangelog } = await import('./core/changelog-cli.js');
|
|
390
|
+
await handleChangelog();
|
|
391
|
+
},
|
|
392
|
+
},
|
|
344
393
|
{
|
|
345
394
|
name: 'last-block',
|
|
346
395
|
description: 'Show the most recent Mech-A/B block event with rule detail (R6-UX2).',
|
|
@@ -365,6 +414,14 @@ const commands = [
|
|
|
365
414
|
await handleMigrate(args);
|
|
366
415
|
},
|
|
367
416
|
},
|
|
417
|
+
{
|
|
418
|
+
name: 'regress-map',
|
|
419
|
+
description: 'Top fix-touched files in the last N days (--days 30 --top 10 --json).',
|
|
420
|
+
handler: async (args) => {
|
|
421
|
+
const { handleRegressMap } = await import('./core/regress-map-cli.js');
|
|
422
|
+
await handleRegressMap(args);
|
|
423
|
+
},
|
|
424
|
+
},
|
|
368
425
|
{
|
|
369
426
|
name: 'suppress-rule',
|
|
370
427
|
description: '[alias: rule suppress] Disable a rule by id/prefix. Hard rules refused.',
|
|
@@ -571,6 +628,12 @@ function printHelp() {
|
|
|
571
628
|
forgen rule <list|suppress|activate|scan|health-scan|classify>
|
|
572
629
|
Rule management (see: forgen rule help)
|
|
573
630
|
forgen stats One-screen trust-layer dashboard (+ philosophy)
|
|
631
|
+
forgen health Single-line health score (0-100) with grade
|
|
632
|
+
forgen probe-workflow arm|report Measure if dynamic-workflow subagents fire hooks (ADR-009 §1)
|
|
633
|
+
forgen workflows install|list Install forgen dynamic-workflow templates to .claude/workflows/
|
|
634
|
+
forgen watch Real-time hook event stream (tail logs live)
|
|
635
|
+
forgen explain [N] Explain the last N block(s) — rule, reason, resolution
|
|
636
|
+
forgen changelog Auto-summarize commits since last release tag
|
|
574
637
|
forgen last-block Show the most recent block event
|
|
575
638
|
forgen recall [--limit N] [--show]
|
|
576
639
|
최근 compound 주입 이력 (solution body preview)
|
|
@@ -585,7 +648,7 @@ function printHelp() {
|
|
|
585
648
|
forgen mcp MCP server management
|
|
586
649
|
forgen skill promote|list Skill management
|
|
587
650
|
forgen notepad show|add|clear Session notepad
|
|
588
|
-
forgen doctor [--prune-state] System diagnostics
|
|
651
|
+
forgen doctor [--quick|--prune-state|--repair] System diagnostics
|
|
589
652
|
forgen uninstall Remove forgen
|
|
590
653
|
|
|
591
654
|
Harness mode (default):
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen changelog — auto-summarize changes since last release tag.
|
|
3
|
+
*
|
|
4
|
+
* Reads git log between the latest vX.Y.Z tag and HEAD, groups by
|
|
5
|
+
* conventional commit type, and outputs a ready-to-paste changelog.
|
|
6
|
+
*/
|
|
7
|
+
export declare function handleChangelog(): Promise<void>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen changelog — auto-summarize changes since last release tag.
|
|
3
|
+
*
|
|
4
|
+
* Reads git log between the latest vX.Y.Z tag and HEAD, groups by
|
|
5
|
+
* conventional commit type, and outputs a ready-to-paste changelog.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
const isTTY = process.stdout.isTTY;
|
|
9
|
+
const C = {
|
|
10
|
+
reset: isTTY ? '\x1b[0m' : '',
|
|
11
|
+
bold: isTTY ? '\x1b[1m' : '',
|
|
12
|
+
dim: isTTY ? '\x1b[2m' : '',
|
|
13
|
+
cyan: isTTY ? '\x1b[36m' : '',
|
|
14
|
+
green: isTTY ? '\x1b[32m' : '',
|
|
15
|
+
yellow: isTTY ? '\x1b[33m' : '',
|
|
16
|
+
};
|
|
17
|
+
function getLatestTag() {
|
|
18
|
+
try {
|
|
19
|
+
return execFileSync('git', ['describe', '--tags', '--abbrev=0'], {
|
|
20
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
21
|
+
}).trim();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getCommitsSince(tag) {
|
|
28
|
+
try {
|
|
29
|
+
const args = tag
|
|
30
|
+
? ['log', `${tag}..HEAD`, '--oneline', '--no-merges']
|
|
31
|
+
: ['log', '--oneline', '--no-merges', '-30'];
|
|
32
|
+
return execFileSync('git', args, {
|
|
33
|
+
encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
|
+
}).trim().split('\n').filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function parseCommit(line) {
|
|
41
|
+
const match = line.match(/^([a-f0-9]+)\s+(\w+)(?:\(([^)]*)\))?:\s*(.+)$/);
|
|
42
|
+
if (match) {
|
|
43
|
+
return { hash: match[1], type: match[2], scope: match[3] ?? '', subject: match[4] };
|
|
44
|
+
}
|
|
45
|
+
const parts = line.split(/\s+/, 2);
|
|
46
|
+
return { hash: parts[0], type: 'other', scope: '', subject: parts.slice(1).join(' ') || line };
|
|
47
|
+
}
|
|
48
|
+
const TYPE_ORDER = {
|
|
49
|
+
feat: { label: 'Features', order: 0 },
|
|
50
|
+
fix: { label: 'Bug Fixes', order: 1 },
|
|
51
|
+
refactor: { label: 'Refactoring', order: 2 },
|
|
52
|
+
test: { label: 'Tests', order: 3 },
|
|
53
|
+
ci: { label: 'CI/CD', order: 4 },
|
|
54
|
+
docs: { label: 'Documentation', order: 5 },
|
|
55
|
+
chore: { label: 'Maintenance', order: 6 },
|
|
56
|
+
other: { label: 'Other', order: 7 },
|
|
57
|
+
};
|
|
58
|
+
export async function handleChangelog() {
|
|
59
|
+
const tag = getLatestTag();
|
|
60
|
+
const rawCommits = getCommitsSince(tag);
|
|
61
|
+
if (rawCommits.length === 0) {
|
|
62
|
+
console.log(`\n ${C.dim}No commits since ${tag ?? 'beginning'}.${C.reset}\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const commits = rawCommits.map(parseCommit);
|
|
66
|
+
const grouped = new Map();
|
|
67
|
+
for (const c of commits) {
|
|
68
|
+
const key = TYPE_ORDER[c.type] ? c.type : 'other';
|
|
69
|
+
if (!grouped.has(key))
|
|
70
|
+
grouped.set(key, []);
|
|
71
|
+
grouped.get(key)?.push(c);
|
|
72
|
+
}
|
|
73
|
+
const sorted = [...grouped.entries()].sort((a, b) => {
|
|
74
|
+
return (TYPE_ORDER[a[0]]?.order ?? 99) - (TYPE_ORDER[b[0]]?.order ?? 99);
|
|
75
|
+
});
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(` ${C.bold}Changelog${C.reset} ${C.dim}${tag ?? 'start'}..HEAD${C.reset} ${C.dim}(${commits.length} commits)${C.reset}`);
|
|
78
|
+
console.log('');
|
|
79
|
+
for (const [type, items] of sorted) {
|
|
80
|
+
const label = TYPE_ORDER[type]?.label ?? type;
|
|
81
|
+
console.log(` ${C.cyan}### ${label}${C.reset}`);
|
|
82
|
+
for (const c of items) {
|
|
83
|
+
const scope = c.scope ? `${C.yellow}(${c.scope})${C.reset} ` : '';
|
|
84
|
+
console.log(` ${C.dim}${c.hash}${C.reset} ${scope}${c.subject}`);
|
|
85
|
+
}
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
|
88
|
+
// Markdown output for copy-paste
|
|
89
|
+
console.log(` ${C.dim}── Markdown (copy-paste ready) ──${C.reset}`);
|
|
90
|
+
console.log('');
|
|
91
|
+
for (const [type, items] of sorted) {
|
|
92
|
+
const label = TYPE_ORDER[type]?.label ?? type;
|
|
93
|
+
console.log(` ### ${label}`);
|
|
94
|
+
for (const c of items) {
|
|
95
|
+
const scope = c.scope ? `**${c.scope}**: ` : '';
|
|
96
|
+
console.log(` - ${scope}${c.subject}`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/core/doctor.d.ts
CHANGED
|
@@ -12,5 +12,8 @@ export interface DoctorOptions {
|
|
|
12
12
|
* doctor 흐름은 정상 종료.
|
|
13
13
|
*/
|
|
14
14
|
repair?: boolean;
|
|
15
|
+
/** When true, run only essential checks (Tools + Plugins + Directories +
|
|
16
|
+
* Initialization Status) for fast onboarding verification. ~10 lines output. */
|
|
17
|
+
quick?: boolean;
|
|
15
18
|
}
|
|
16
19
|
export declare function runDoctor(opts?: DoctorOptions): Promise<void>;
|
package/dist/core/doctor.js
CHANGED
|
@@ -7,6 +7,8 @@ import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_S
|
|
|
7
7
|
import { getTimingStats } from '../hooks/shared/hook-timing.js';
|
|
8
8
|
import { countSessionScopedFiles, pruneState } from './state-gc.js';
|
|
9
9
|
import { summarizeAllByHost } from '../store/host-mismatch.js';
|
|
10
|
+
import { readForgeLoopState } from '../hooks/shared/forge-loop-state.js';
|
|
11
|
+
import { effortAdvisory } from './effort-advisory.js';
|
|
10
12
|
/** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
|
|
11
13
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
12
14
|
let currentSection = '';
|
|
@@ -189,6 +191,28 @@ export async function runDoctor(opts = {}) {
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
console.log();
|
|
194
|
+
section('Initialization Status');
|
|
195
|
+
const profilePath = path.join(ME_DIR, 'forge-profile.json');
|
|
196
|
+
const profileOk = exists(profilePath);
|
|
197
|
+
check('Profile exists (forge-profile.json)', profileOk, 'No profile — run `forgen` to complete onboarding');
|
|
198
|
+
const hooksWired = forgenPluginCacheOk || pluginRegistered;
|
|
199
|
+
if (hooksWired && !profileOk) {
|
|
200
|
+
check('Hooks wired + profile ready', false, 'Hooks are active but personalization is disabled — run `forgen` to onboard');
|
|
201
|
+
}
|
|
202
|
+
else if (hooksWired && profileOk) {
|
|
203
|
+
check('Hooks wired + profile ready', true);
|
|
204
|
+
}
|
|
205
|
+
console.log();
|
|
206
|
+
if (opts.quick) {
|
|
207
|
+
console.log();
|
|
208
|
+
if (failedChecks.length === 0) {
|
|
209
|
+
console.log(' All essential checks passed.\n');
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
console.log(` ${failedChecks.length} issue(s) found. Run \`forgen doctor\` for full diagnostics.\n`);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
192
216
|
section('Environment');
|
|
193
217
|
check('Inside tmux session', !!process.env.TMUX, 'FORGEN auto-compound relies on tmux. Launch: tmux new -s forgen');
|
|
194
218
|
check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1', 'Set by `forgen` / `fgx` launcher. Hooks assume harness mode is active.');
|
|
@@ -491,12 +515,18 @@ export async function runDoctor(opts = {}) {
|
|
|
491
515
|
}
|
|
492
516
|
// P4 셀프 가드: fix:feat 비율 30% 초과 시 회귀 패턴 의심 경고.
|
|
493
517
|
try {
|
|
494
|
-
const { computeFixFeatRatio, formatFixRatio } = await import('./git-stats.js');
|
|
518
|
+
const { computeFixFeatRatio, formatFixRatio, computeRegressMap } = await import('./git-stats.js');
|
|
495
519
|
const ratio = computeFixFeatRatio();
|
|
496
520
|
if (ratio.available) {
|
|
497
521
|
console.log(` ${formatFixRatio(ratio)}`);
|
|
498
522
|
if (ratio.exceedsThreshold) {
|
|
499
523
|
console.log(' ⚠ fix:feat 비율이 임계값을 초과했습니다. "이거 고치면 저거 버그난다" 패턴 의심 — 검증 레이어 invariant 점검 권장.');
|
|
524
|
+
const map = computeRegressMap(process.cwd(), 30, 3);
|
|
525
|
+
if (map.available && map.hotspots.length > 0) {
|
|
526
|
+
const top = map.hotspots.map((h) => `${h.path} (${h.fixHits})`).join(', ');
|
|
527
|
+
console.log(` → 진앙 후보 top 3: ${top}`);
|
|
528
|
+
console.log(' → 전체 보기: forgen regress-map');
|
|
529
|
+
}
|
|
500
530
|
}
|
|
501
531
|
}
|
|
502
532
|
}
|
|
@@ -529,6 +559,50 @@ export async function runDoctor(opts = {}) {
|
|
|
529
559
|
// [Codex Parity] — parity-result.json 신선도 검사 (v0.4.2 패턴 확장)
|
|
530
560
|
renderCodexParity();
|
|
531
561
|
console.log();
|
|
562
|
+
// [ψ-long] — within-conversation compound 효과 측정 (v0.4.10)
|
|
563
|
+
// 측정 시간이 길어 ship-gate 는 advisory. 신선도(<= 14d) + psiLong > 0 만 확인.
|
|
564
|
+
console.log(' [ψ-long compound]');
|
|
565
|
+
try {
|
|
566
|
+
const psiLongPath = path.join(STATE_DIR, 'psi-long-result.json');
|
|
567
|
+
if (!fs.existsSync(psiLongPath)) {
|
|
568
|
+
console.log(' ○ no measurement yet — `pnpm --filter @wooojin/forgen-eval psi:long` 권장');
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const raw = fs.readFileSync(psiLongPath, 'utf-8');
|
|
572
|
+
const data = JSON.parse(raw);
|
|
573
|
+
const ageMs = data.at ? Date.now() - new Date(data.at).getTime() : Number.POSITIVE_INFINITY;
|
|
574
|
+
const ageDays = Math.floor(ageMs / (24 * 60 * 60 * 1000));
|
|
575
|
+
const psiLong = data.psiLong ?? 0;
|
|
576
|
+
const passed = !!data.passed;
|
|
577
|
+
const freshTag = ageDays <= 14 ? 'fresh' : `${ageDays}d stale`;
|
|
578
|
+
const gateLabel = passed ? '✓ PASS' : '✗ FAIL';
|
|
579
|
+
console.log(` ${gateLabel} ψ_long=${psiLong.toFixed(3)} N=${data.N ?? '?'} (${freshTag})`);
|
|
580
|
+
if (!passed) {
|
|
581
|
+
console.log(' ⚠ within-session compound 효과 미관측 — correction injection / rule trigger 경로 점검');
|
|
582
|
+
}
|
|
583
|
+
else if (ageDays > 14) {
|
|
584
|
+
console.log(' △ 측정 14일 초과 — 재측정 권장 (`pnpm --filter @wooojin/forgen-eval psi:long`)');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
console.log(` Unable to read psi-long state: ${e instanceof Error ? e.message : 'unknown'}`);
|
|
590
|
+
}
|
|
591
|
+
console.log();
|
|
592
|
+
// [Effort (Opus 4.8)] — ADR-009 §5. nudge-only: forgen 은 effort 를 직접 설정할 수
|
|
593
|
+
// 없으므로 long-running 컨텍스트(forge-loop)에서 xhigh/ultracode 를 권고만 한다.
|
|
594
|
+
console.log(' [Effort (Opus 4.8)]');
|
|
595
|
+
try {
|
|
596
|
+
const loopActive = !!readForgeLoopState()?.active;
|
|
597
|
+
const adv = effortAdvisory({ longRunningActive: loopActive });
|
|
598
|
+
const icon = adv.recommend === 'xhigh' ? '→' : '✓';
|
|
599
|
+
console.log(` ${icon} recommend: ${adv.recommend}`);
|
|
600
|
+
console.log(` ${adv.reason}`);
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
console.log(' Unable to compute effort advisory.');
|
|
604
|
+
}
|
|
605
|
+
console.log();
|
|
532
606
|
// [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
|
|
533
607
|
console.log(' [Summary]');
|
|
534
608
|
if (failedChecks.length === 0) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen effort advisory (ADR-009 §5) — nudge-only.
|
|
3
|
+
*
|
|
4
|
+
* Claude Opus 4.8 은 effort(high/xhigh)·ultracode 를 도입했다. forgen 은 hook
|
|
5
|
+
* 인터페이스를 통해 동작하므로 Claude 의 effort 를 **프로그램적으로 설정할 수 없다**.
|
|
6
|
+
* 따라서 이 모듈은 *권고만* 한다 — long-running/비동기 작업(forge-loop, 대규모
|
|
7
|
+
* 워크플로우)에서는 xhigh/ultracode 가 유리하다는 힌트를 사용자에게 노출한다.
|
|
8
|
+
*
|
|
9
|
+
* 순수 함수 (IO 없음) — doctor 등에서 surface.
|
|
10
|
+
*/
|
|
11
|
+
export interface EffortAdvisory {
|
|
12
|
+
recommend: 'high' | 'xhigh';
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface EffortContext {
|
|
16
|
+
/** forge-loop 등 장시간 무인 실행이 활성인가. */
|
|
17
|
+
longRunningActive: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* effort 권고를 반환한다. long-running 컨텍스트면 xhigh, 아니면 high(기본).
|
|
21
|
+
* 4.8 기본값이 이미 high 이므로 일상 작업엔 추가 권고가 불필요하다.
|
|
22
|
+
*/
|
|
23
|
+
export declare function effortAdvisory(ctx: EffortContext): EffortAdvisory;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen effort advisory (ADR-009 §5) — nudge-only.
|
|
3
|
+
*
|
|
4
|
+
* Claude Opus 4.8 은 effort(high/xhigh)·ultracode 를 도입했다. forgen 은 hook
|
|
5
|
+
* 인터페이스를 통해 동작하므로 Claude 의 effort 를 **프로그램적으로 설정할 수 없다**.
|
|
6
|
+
* 따라서 이 모듈은 *권고만* 한다 — long-running/비동기 작업(forge-loop, 대규모
|
|
7
|
+
* 워크플로우)에서는 xhigh/ultracode 가 유리하다는 힌트를 사용자에게 노출한다.
|
|
8
|
+
*
|
|
9
|
+
* 순수 함수 (IO 없음) — doctor 등에서 surface.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* effort 권고를 반환한다. long-running 컨텍스트면 xhigh, 아니면 high(기본).
|
|
13
|
+
* 4.8 기본값이 이미 high 이므로 일상 작업엔 추가 권고가 불필요하다.
|
|
14
|
+
*/
|
|
15
|
+
export function effortAdvisory(ctx) {
|
|
16
|
+
if (ctx.longRunningActive) {
|
|
17
|
+
return {
|
|
18
|
+
recommend: 'xhigh',
|
|
19
|
+
reason: 'long-running 컨텍스트 감지(forge-loop). Opus 4.8 은 어려운/비동기 작업에 ' +
|
|
20
|
+
'xhigh(=extra) 또는 /effort ultracode 를 권장 — forgen 은 effort 를 직접 설정할 수 ' +
|
|
21
|
+
'없으니 /effort 로 수동 전환하세요. (nudge-only)',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
recommend: 'high',
|
|
26
|
+
reason: 'Opus 4.8 기본 effort=high 로 충분. 대규모 마이그레이션/리팩터/감사 시에만 ' +
|
|
27
|
+
'/effort xhigh|ultracode 고려.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen explain — explain the most recent block in detail.
|
|
3
|
+
*
|
|
4
|
+
* Shows: what rule fired, why, what Claude said, and how to resolve.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { STATE_DIR } from './paths.js';
|
|
9
|
+
const isTTY = process.stdout.isTTY;
|
|
10
|
+
const C = {
|
|
11
|
+
reset: isTTY ? '\x1b[0m' : '',
|
|
12
|
+
bold: isTTY ? '\x1b[1m' : '',
|
|
13
|
+
dim: isTTY ? '\x1b[2m' : '',
|
|
14
|
+
red: isTTY ? '\x1b[31m' : '',
|
|
15
|
+
green: isTTY ? '\x1b[32m' : '',
|
|
16
|
+
yellow: isTTY ? '\x1b[33m' : '',
|
|
17
|
+
cyan: isTTY ? '\x1b[36m' : '',
|
|
18
|
+
magenta: isTTY ? '\x1b[35m' : '',
|
|
19
|
+
};
|
|
20
|
+
function readViolations() {
|
|
21
|
+
const p = path.join(STATE_DIR, 'enforcement', 'violations.jsonl');
|
|
22
|
+
if (!fs.existsSync(p))
|
|
23
|
+
return [];
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
26
|
+
if (!line.trim())
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
out.push(JSON.parse(line));
|
|
30
|
+
}
|
|
31
|
+
catch { /* skip */ }
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function readAcknowledgments() {
|
|
36
|
+
const p = path.join(STATE_DIR, 'enforcement', 'acknowledgments.jsonl');
|
|
37
|
+
if (!fs.existsSync(p))
|
|
38
|
+
return [];
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
41
|
+
if (!line.trim())
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
out.push(JSON.parse(line));
|
|
45
|
+
}
|
|
46
|
+
catch { /* skip */ }
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function formatTime(iso) {
|
|
51
|
+
try {
|
|
52
|
+
const d = new Date(iso);
|
|
53
|
+
return d.toLocaleString('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return iso;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function handleExplain(args) {
|
|
60
|
+
const violations = readViolations();
|
|
61
|
+
if (violations.length === 0) {
|
|
62
|
+
console.log(`\n ${C.green}No blocks recorded.${C.reset} forgen hasn't blocked Claude yet.\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const count = Math.min(Number(args[0]) || 1, 5);
|
|
66
|
+
const targets = violations.slice(-count);
|
|
67
|
+
const acks = readAcknowledgments();
|
|
68
|
+
for (const v of targets) {
|
|
69
|
+
const ruleId = v.rule_id ?? v.rule ?? v.guard ?? 'unknown';
|
|
70
|
+
const source = v.source ?? 'unknown';
|
|
71
|
+
const kind = v.kind ?? 'block';
|
|
72
|
+
const when = v.at ? formatTime(v.at) : 'unknown time';
|
|
73
|
+
const reason = v.reason ?? v.reason_preview ?? v.message_preview ?? v.pattern_preview ?? '(no reason recorded)';
|
|
74
|
+
// Check if this block was acknowledged
|
|
75
|
+
const blockTime = v.at ? new Date(v.at).getTime() : 0;
|
|
76
|
+
const wasAcked = acks.some(a => {
|
|
77
|
+
if (!a.at)
|
|
78
|
+
return false;
|
|
79
|
+
const ackTime = new Date(a.at).getTime();
|
|
80
|
+
return ackTime > blockTime && ackTime - blockTime < 300_000; // within 5 min
|
|
81
|
+
});
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(` ${C.red}${C.bold}BLOCK${C.reset} ${C.dim}${when}${C.reset}`);
|
|
84
|
+
console.log(` ${C.cyan}Rule:${C.reset} ${ruleId}`);
|
|
85
|
+
console.log(` ${C.cyan}Source:${C.reset} ${source} (${kind})`);
|
|
86
|
+
if (v.tool) {
|
|
87
|
+
console.log(` ${C.cyan}Tool:${C.reset} ${v.tool}`);
|
|
88
|
+
}
|
|
89
|
+
console.log(` ${C.cyan}Reason:${C.reset}`);
|
|
90
|
+
for (const line of reason.split('\n').slice(0, 5)) {
|
|
91
|
+
console.log(` ${C.dim}${line}${C.reset}`);
|
|
92
|
+
}
|
|
93
|
+
console.log(` ${C.cyan}Resolved:${C.reset} ${wasAcked ? `${C.green}Yes — Claude retracted and resubmitted with evidence${C.reset}` : `${C.yellow}No acknowledgment found${C.reset}`}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(` ${C.dim}To suppress this rule: forgen suppress-rule ${ruleId}${C.reset}`);
|
|
96
|
+
console.log(` ${C.dim}To bypass one turn: set FORGEN_USER_CONFIRMED=1${C.reset}`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
package/dist/core/git-stats.d.ts
CHANGED
|
@@ -34,3 +34,26 @@ export declare function computeFixFeatRatio(cwd?: string, windowSize?: number, t
|
|
|
34
34
|
export declare function parseGitLog(rawLog: string, windowSize?: number, threshold?: number): FixRatioStats;
|
|
35
35
|
/** 사람용 한 줄 라벨. */
|
|
36
36
|
export declare function formatFixRatio(s: FixRatioStats): string;
|
|
37
|
+
export interface RegressHotspot {
|
|
38
|
+
path: string;
|
|
39
|
+
fixHits: number;
|
|
40
|
+
lastFixSha: string;
|
|
41
|
+
lastFixDate: string;
|
|
42
|
+
}
|
|
43
|
+
export interface RegressMap {
|
|
44
|
+
windowDays: number;
|
|
45
|
+
fixCommits: number;
|
|
46
|
+
hotspots: RegressHotspot[];
|
|
47
|
+
available: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 지난 N일 동안 fix 커밋이 닿은 파일 hot-list.
|
|
51
|
+
*
|
|
52
|
+
* doctor 가 fix:feat 비율로 "회귀 패턴 의심" 시그널은 주지만, 진앙은 안 보여준다.
|
|
53
|
+
* regress-map 은 fix 커밋의 changed-files 를 카운트해 top-N hotspot 을 추출 →
|
|
54
|
+
* "이거 고치면 저거 깨진다" 의 *저거* 후보를 한 화면에 노출.
|
|
55
|
+
*
|
|
56
|
+
* 제외 scope (fix:feat 와 동일): fix(test|tests|docs|doc) — 노이즈.
|
|
57
|
+
*/
|
|
58
|
+
export declare function computeRegressMap(cwd?: string, windowDays?: number, topN?: number): RegressMap;
|
|
59
|
+
export declare function parseRegressLog(raw: string, windowDays: number, topN: number): RegressMap;
|
package/dist/core/git-stats.js
CHANGED
|
@@ -77,3 +77,53 @@ export function formatFixRatio(s) {
|
|
|
77
77
|
const flag = s.exceedsThreshold ? ` ⚠ over ${thresholdPct}%` : '';
|
|
78
78
|
return `fix:feat ratio ${pct}% (${s.fixCount}/${s.fixCount + s.featCount} in last ${s.windowSize})${flag}`;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* 지난 N일 동안 fix 커밋이 닿은 파일 hot-list.
|
|
82
|
+
*
|
|
83
|
+
* doctor 가 fix:feat 비율로 "회귀 패턴 의심" 시그널은 주지만, 진앙은 안 보여준다.
|
|
84
|
+
* regress-map 은 fix 커밋의 changed-files 를 카운트해 top-N hotspot 을 추출 →
|
|
85
|
+
* "이거 고치면 저거 깨진다" 의 *저거* 후보를 한 화면에 노출.
|
|
86
|
+
*
|
|
87
|
+
* 제외 scope (fix:feat 와 동일): fix(test|tests|docs|doc) — 노이즈.
|
|
88
|
+
*/
|
|
89
|
+
export function computeRegressMap(cwd = process.cwd(), windowDays = 30, topN = 10) {
|
|
90
|
+
try {
|
|
91
|
+
const since = `--since=${windowDays}.days.ago`;
|
|
92
|
+
const out = execFileSync('git', ['log', '--no-merges', since, '--name-only', '--pretty=format:%x1fCOMMIT%x1f%h%x1f%cs%x1f%s'], { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 });
|
|
93
|
+
return parseRegressLog(out, windowDays, topN);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { windowDays, fixCommits: 0, hotspots: [], available: false };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function parseRegressLog(raw, windowDays, topN) {
|
|
100
|
+
const blocks = raw.split('\x1fCOMMIT\x1f').slice(1);
|
|
101
|
+
const counter = new Map();
|
|
102
|
+
let fixCommits = 0;
|
|
103
|
+
for (const block of blocks) {
|
|
104
|
+
const [sha, date, rest = ''] = block.split('\x1f');
|
|
105
|
+
const nl = rest.indexOf('\n');
|
|
106
|
+
const subject = nl === -1 ? rest : rest.slice(0, nl);
|
|
107
|
+
const files = nl === -1 ? [] : rest.slice(nl + 1).split('\n').map((s) => s.trim()).filter(Boolean);
|
|
108
|
+
const m = subject.match(/^(fix|feat)(?:\(([^)]+)\))?:/);
|
|
109
|
+
if (!m || m[1] !== 'fix')
|
|
110
|
+
continue;
|
|
111
|
+
const scope = (m[2] ?? '').toLowerCase().trim();
|
|
112
|
+
if (SCOPE_EXCLUSIONS.has(scope))
|
|
113
|
+
continue;
|
|
114
|
+
fixCommits++;
|
|
115
|
+
for (const f of files) {
|
|
116
|
+
const existing = counter.get(f);
|
|
117
|
+
if (existing) {
|
|
118
|
+
existing.fixHits++;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
counter.set(f, { path: f, fixHits: 1, lastFixSha: sha, lastFixDate: date });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const hotspots = Array.from(counter.values())
|
|
126
|
+
.sort((a, b) => b.fixHits - a.fixHits || a.path.localeCompare(b.path))
|
|
127
|
+
.slice(0, topN);
|
|
128
|
+
return { windowDays, fixCommits, hotspots, available: true };
|
|
129
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen health — single-line health score (0-100).
|
|
3
|
+
*
|
|
4
|
+
* Combines:
|
|
5
|
+
* - Solution utilization (7d match rate) 30%
|
|
6
|
+
* - Block→ack effectiveness 25%
|
|
7
|
+
* - Knowledge growth (extractions this week) 20%
|
|
8
|
+
* - Rule coverage (active rules) 15%
|
|
9
|
+
* - Profile completeness 10%
|
|
10
|
+
*/
|
|
11
|
+
export interface HealthScore {
|
|
12
|
+
total: number;
|
|
13
|
+
components: {
|
|
14
|
+
utilization: number;
|
|
15
|
+
effectiveness: number;
|
|
16
|
+
growth: number;
|
|
17
|
+
coverage: number;
|
|
18
|
+
profile: number;
|
|
19
|
+
};
|
|
20
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
21
|
+
}
|
|
22
|
+
export declare function computeHealth(): HealthScore;
|
|
23
|
+
export declare function handleHealth(): Promise<void>;
|