@wooojin/forgen 0.4.10 → 0.4.12
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 +62 -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 +57 -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 +38 -0
- 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/health-cli.d.ts +23 -0
- package/dist/core/health-cli.js +86 -0
- package/dist/core/probe-workflow-cli.d.ts +72 -0
- package/dist/core/probe-workflow-cli.js +282 -0
- package/dist/core/spawn.d.ts +13 -0
- package/dist/core/spawn.js +36 -8
- 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.js +1 -1
- package/dist/hooks/notepad-injector.js +7 -0
- package/dist/hooks/post-tool-use.js +8 -1
- package/dist/hooks/secret-filter.d.ts +1 -0
- package/dist/hooks/secret-filter.js +17 -7
- 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/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).',
|
|
@@ -579,6 +628,12 @@ function printHelp() {
|
|
|
579
628
|
forgen rule <list|suppress|activate|scan|health-scan|classify>
|
|
580
629
|
Rule management (see: forgen rule help)
|
|
581
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
|
|
582
637
|
forgen last-block Show the most recent block event
|
|
583
638
|
forgen recall [--limit N] [--show]
|
|
584
639
|
최근 compound 주입 이력 (solution body preview)
|
|
@@ -593,7 +648,7 @@ function printHelp() {
|
|
|
593
648
|
forgen mcp MCP server management
|
|
594
649
|
forgen skill promote|list Skill management
|
|
595
650
|
forgen notepad show|add|clear Session notepad
|
|
596
|
-
forgen doctor [--prune-state] System diagnostics
|
|
651
|
+
forgen doctor [--quick|--prune-state|--repair] System diagnostics
|
|
597
652
|
forgen uninstall Remove forgen
|
|
598
653
|
|
|
599
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.');
|
|
@@ -565,6 +589,20 @@ export async function runDoctor(opts = {}) {
|
|
|
565
589
|
console.log(` Unable to read psi-long state: ${e instanceof Error ? e.message : 'unknown'}`);
|
|
566
590
|
}
|
|
567
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();
|
|
568
606
|
// [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
|
|
569
607
|
console.log(' [Summary]');
|
|
570
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
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
import { computeStats } from './stats-cli.js';
|
|
12
|
+
const isTTY = process.stdout.isTTY;
|
|
13
|
+
const C = {
|
|
14
|
+
reset: isTTY ? '\x1b[0m' : '',
|
|
15
|
+
bold: isTTY ? '\x1b[1m' : '',
|
|
16
|
+
dim: isTTY ? '\x1b[2m' : '',
|
|
17
|
+
green: isTTY ? '\x1b[32m' : '',
|
|
18
|
+
yellow: isTTY ? '\x1b[33m' : '',
|
|
19
|
+
red: isTTY ? '\x1b[31m' : '',
|
|
20
|
+
cyan: isTTY ? '\x1b[36m' : '',
|
|
21
|
+
};
|
|
22
|
+
export function computeHealth() {
|
|
23
|
+
const s = computeStats();
|
|
24
|
+
// 1. Utilization (30%): % of solutions matched in last 7d, capped at 100%
|
|
25
|
+
const utilization = Math.min(1, s.solutionHealth.utilization7d) * 30;
|
|
26
|
+
// 2. Effectiveness (25%): if blocks happened, what % were acknowledged
|
|
27
|
+
let effectiveness;
|
|
28
|
+
if (s.blocks7d === 0) {
|
|
29
|
+
effectiveness = 25; // no blocks = no problems
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
effectiveness = (s.acks7d / s.blocks7d) * 25;
|
|
33
|
+
}
|
|
34
|
+
// 3. Growth (20%): extractions this week (1 extraction = 10pts, cap at 20)
|
|
35
|
+
const growth = Math.min(20, s.weeklyTrend.extractionsThisWeek * 10);
|
|
36
|
+
// 4. Coverage (15%): active rules (1 rule = 3pts, cap at 15)
|
|
37
|
+
const coverage = Math.min(15, s.activeRules * 3);
|
|
38
|
+
// 5. Profile (10%): has profile + has philosophy + axis scores populated
|
|
39
|
+
let profile = 0;
|
|
40
|
+
if (s.philosophy) {
|
|
41
|
+
profile += 4; // profile exists
|
|
42
|
+
if (s.philosophy.basePacks.length > 0)
|
|
43
|
+
profile += 3;
|
|
44
|
+
if (Object.keys(s.philosophy.axisScores).length >= 4)
|
|
45
|
+
profile += 3;
|
|
46
|
+
}
|
|
47
|
+
const total = Math.round(utilization + effectiveness + growth + coverage + profile);
|
|
48
|
+
const grade = total >= 80 ? 'A' : total >= 60 ? 'B' : total >= 40 ? 'C' : total >= 20 ? 'D' : 'F';
|
|
49
|
+
return {
|
|
50
|
+
total,
|
|
51
|
+
components: {
|
|
52
|
+
utilization: Math.round(utilization),
|
|
53
|
+
effectiveness: Math.round(effectiveness),
|
|
54
|
+
growth: Math.round(growth),
|
|
55
|
+
coverage: Math.round(coverage),
|
|
56
|
+
profile: Math.round(profile),
|
|
57
|
+
},
|
|
58
|
+
grade,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function gradeColor(grade) {
|
|
62
|
+
if (grade === 'A')
|
|
63
|
+
return C.green;
|
|
64
|
+
if (grade === 'B')
|
|
65
|
+
return C.cyan;
|
|
66
|
+
if (grade === 'C')
|
|
67
|
+
return C.yellow;
|
|
68
|
+
return C.red;
|
|
69
|
+
}
|
|
70
|
+
function bar(value, max, width = 10) {
|
|
71
|
+
const filled = Math.round((value / max) * width);
|
|
72
|
+
return '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, width - filled));
|
|
73
|
+
}
|
|
74
|
+
export async function handleHealth() {
|
|
75
|
+
const h = computeHealth();
|
|
76
|
+
const gc = gradeColor(h.grade);
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(` ${C.bold}forgen health${C.reset} ${gc}${C.bold}${h.grade}${C.reset} ${gc}${h.total}/100${C.reset}`);
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(` Utilization ${bar(h.components.utilization, 30)} ${h.components.utilization}/30 ${C.dim}solution match rate (7d)${C.reset}`);
|
|
81
|
+
console.log(` Effectiveness ${bar(h.components.effectiveness, 25)} ${h.components.effectiveness}/25 ${C.dim}block→ack ratio${C.reset}`);
|
|
82
|
+
console.log(` Growth ${bar(h.components.growth, 20)} ${h.components.growth}/20 ${C.dim}extractions this week${C.reset}`);
|
|
83
|
+
console.log(` Coverage ${bar(h.components.coverage, 15)} ${h.components.coverage}/15 ${C.dim}active rules${C.reset}`);
|
|
84
|
+
console.log(` Profile ${bar(h.components.profile, 10)} ${h.components.profile}/10 ${C.dim}personalization depth${C.reset}`);
|
|
85
|
+
console.log('');
|
|
86
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgen probe-workflow — ADR-009 §1 결정적 미확인 변수 실측 도구.
|
|
3
|
+
*
|
|
4
|
+
* 질문: dynamic workflow **내부** 에이전트에 대해 SubagentStart/Stop·PostToolUse
|
|
5
|
+
* 훅이 발화하는가? (워크플로우 런타임은 "대화와 분리된 격리 백그라운드"로
|
|
6
|
+
* 명시되어 있어 발화 여부가 문서로 미확인.)
|
|
7
|
+
*
|
|
8
|
+
* 이 답이 ADR-009 §2(훅 라우트로 워크플로우까지 검증 가능) vs §3(워크플로우
|
|
9
|
+
* 품질은 템플릿 라우트로만 도달)의 선택을 가른다. forgen 프로젝트 룰상 가정
|
|
10
|
+
* 위에 §2 를 구현할 수 없으므로, 실제 실행 증거를 먼저 수집한다.
|
|
11
|
+
*
|
|
12
|
+
* 신호원 (훅이 부작용으로 남기는 state 파일 — forgen 자체 timing 계측의 공백과
|
|
13
|
+
* 무관하게 직접 관측 가능):
|
|
14
|
+
* - SubagentStart/Stop → ~/.forgen/state/active-agents-*.json (agents[])
|
|
15
|
+
* - PostToolUse → ~/.forgen/state/modified-files-*.json (mtime)
|
|
16
|
+
* - (보조) hook-timing.jsonl 의 event 별 엔트리
|
|
17
|
+
*
|
|
18
|
+
* 절차 (2단계 — 런타임이 격리되어 있어 단일 프로세스로는 트리거 불가):
|
|
19
|
+
* 1. `forgen probe-workflow arm` → baseline 마커 기록 + 안내 출력
|
|
20
|
+
* 2. 사용자가 Claude Code 에서 워크플로우 1회 실행 (그 사이 다른 작업 금지)
|
|
21
|
+
* 3. `forgen probe-workflow report` → baseline 이후 신호 수집 → verdict 박제
|
|
22
|
+
*
|
|
23
|
+
* 가정 (detailed-communication): arm~report 사이에 사용자가 **워크플로우만**
|
|
24
|
+
* 실행했다고 전제한다. 일반 Task-tool subagent 를 같이 돌리면 신호가 섞인다.
|
|
25
|
+
*/
|
|
26
|
+
export interface ProbeBaseline {
|
|
27
|
+
armedAtMs: number;
|
|
28
|
+
armedIso: string;
|
|
29
|
+
}
|
|
30
|
+
export interface AgentObservation {
|
|
31
|
+
agentId: string;
|
|
32
|
+
agentType?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
startedAtMs: number;
|
|
35
|
+
stoppedAtMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ProbeObservations {
|
|
38
|
+
agents: AgentObservation[];
|
|
39
|
+
/** baseline 이후 modified-files-*.json 이 갱신됨 (PostToolUse 발화 신호). */
|
|
40
|
+
postToolUseFired: boolean;
|
|
41
|
+
/** hook-timing.jsonl 에서 baseline 이후 관측된 event 이름들. */
|
|
42
|
+
hookEvents: string[];
|
|
43
|
+
}
|
|
44
|
+
export type ProbeOutcome = 'workflow-hooks-fire' | 'workflow-hooks-absent' | 'inconclusive';
|
|
45
|
+
export interface ProbeVerdict {
|
|
46
|
+
subagentStartStopFired: boolean;
|
|
47
|
+
postToolUseFired: boolean;
|
|
48
|
+
agentCount: number;
|
|
49
|
+
maxConcurrency: number;
|
|
50
|
+
agentTypes: string[];
|
|
51
|
+
outcome: ProbeOutcome;
|
|
52
|
+
recommendation: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 구간 [start, stop) 들의 최대 동시 겹침 수. stoppedAt 미지정(진행 중) 은 +∞ 로 간주.
|
|
56
|
+
* sweep-line: start 이벤트 +1, stop 이벤트 -1 을 시간순 정렬 후 누적 최대.
|
|
57
|
+
* 동일 시각에서는 start(+1) 를 stop(-1) 보다 먼저 처리해 겹침을 과소평가하지 않는다.
|
|
58
|
+
*/
|
|
59
|
+
export declare function maxConcurrency(agents: AgentObservation[]): number;
|
|
60
|
+
/**
|
|
61
|
+
* Pure core — baseline + 관측치 → verdict. IO 없음 (단위 테스트 대상).
|
|
62
|
+
*
|
|
63
|
+
* 판정:
|
|
64
|
+
* - 에이전트 0 → 'workflow-hooks-absent' (단 "워크플로우를 실제로 띄웠는가"
|
|
65
|
+
* 확인 전제 — recommendation 에 명시). §2 는 워크플로우 내부에 도달 못 함.
|
|
66
|
+
* - 에이전트 >0 → 'workflow-hooks-fire'. 동시 ≥11 이면 워크플로우 강한 신호.
|
|
67
|
+
* §2(SubagentStop 검증)가 워크플로우까지 커버 가능.
|
|
68
|
+
*/
|
|
69
|
+
export declare function analyzeProbe(obs: ProbeObservations): ProbeVerdict;
|
|
70
|
+
/** baseline 이후 신호 수집 (IO 셸). */
|
|
71
|
+
export declare function collectObservations(baselineMs: number): ProbeObservations;
|
|
72
|
+
export declare function handleProbeWorkflow(args: string[]): Promise<void>;
|