@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.
Files changed (76) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +62 -0
  3. package/README.md +33 -1
  4. package/assets/claude/agents/forgen-verify.md +65 -0
  5. package/assets/claude/workflows/compound-extract.js +136 -0
  6. package/assets/claude/workflows/evidence-gate-audit.js +107 -0
  7. package/assets/shared/hook-registry.json +1 -0
  8. package/dist/checks/_shared/meta-guard-dispatch.d.ts +38 -0
  9. package/dist/checks/_shared/meta-guard-dispatch.js +80 -0
  10. package/dist/checks/_shared/text-sanitizer.js +15 -0
  11. package/dist/cli.js +57 -2
  12. package/dist/core/changelog-cli.d.ts +7 -0
  13. package/dist/core/changelog-cli.js +100 -0
  14. package/dist/core/doctor.d.ts +3 -0
  15. package/dist/core/doctor.js +38 -0
  16. package/dist/core/effort-advisory.d.ts +23 -0
  17. package/dist/core/effort-advisory.js +29 -0
  18. package/dist/core/explain-cli.d.ts +6 -0
  19. package/dist/core/explain-cli.js +99 -0
  20. package/dist/core/health-cli.d.ts +23 -0
  21. package/dist/core/health-cli.js +86 -0
  22. package/dist/core/probe-workflow-cli.d.ts +72 -0
  23. package/dist/core/probe-workflow-cli.js +282 -0
  24. package/dist/core/spawn.d.ts +13 -0
  25. package/dist/core/spawn.js +36 -8
  26. package/dist/core/stats-cli.d.ts +22 -9
  27. package/dist/core/stats-cli.js +149 -0
  28. package/dist/core/watch-cli.d.ts +7 -0
  29. package/dist/core/watch-cli.js +185 -0
  30. package/dist/core/workflows-cli.d.ts +26 -0
  31. package/dist/core/workflows-cli.js +120 -0
  32. package/dist/engine/compound-export.d.ts +12 -0
  33. package/dist/engine/compound-export.js +136 -14
  34. package/dist/engine/compound-extractor.d.ts +12 -43
  35. package/dist/engine/compound-extractor.js +27 -756
  36. package/dist/engine/extraction-diff.d.ts +11 -0
  37. package/dist/engine/extraction-diff.js +105 -0
  38. package/dist/engine/extraction-gates.d.ts +37 -0
  39. package/dist/engine/extraction-gates.js +100 -0
  40. package/dist/engine/extraction-git.d.ts +20 -0
  41. package/dist/engine/extraction-git.js +75 -0
  42. package/dist/engine/extraction-persistence.d.ts +27 -0
  43. package/dist/engine/extraction-persistence.js +140 -0
  44. package/dist/engine/extraction-session.d.ts +26 -0
  45. package/dist/engine/extraction-session.js +230 -0
  46. package/dist/engine/lifecycle/types.d.ts +1 -1
  47. package/dist/engine/meta-learning/matcher-weight-loader.d.ts +16 -0
  48. package/dist/engine/meta-learning/matcher-weight-loader.js +45 -0
  49. package/dist/engine/precision-guards.d.ts +14 -0
  50. package/dist/engine/precision-guards.js +39 -0
  51. package/dist/engine/ranking-pipeline.d.ts +45 -0
  52. package/dist/engine/ranking-pipeline.js +66 -0
  53. package/dist/engine/relevance-scorer.d.ts +43 -0
  54. package/dist/engine/relevance-scorer.js +81 -0
  55. package/dist/engine/scoring-algorithms.d.ts +31 -0
  56. package/dist/engine/scoring-algorithms.js +109 -0
  57. package/dist/engine/solution-matcher-eval.d.ts +97 -0
  58. package/dist/engine/solution-matcher-eval.js +122 -0
  59. package/dist/engine/solution-matcher.d.ts +21 -380
  60. package/dist/engine/solution-matcher.js +27 -828
  61. package/dist/fgx.js +1 -1
  62. package/dist/hooks/notepad-injector.js +7 -0
  63. package/dist/hooks/post-tool-use.js +8 -1
  64. package/dist/hooks/secret-filter.d.ts +1 -0
  65. package/dist/hooks/secret-filter.js +17 -7
  66. package/dist/hooks/shared/preflight-check.d.ts +15 -0
  67. package/dist/hooks/shared/preflight-check.js +51 -0
  68. package/dist/hooks/stop-guard.js +19 -60
  69. package/dist/hooks/subagent-stop-guard.d.ts +23 -0
  70. package/dist/hooks/subagent-stop-guard.js +158 -0
  71. package/dist/hooks/subagent-tracker.d.ts +36 -3
  72. package/dist/hooks/subagent-tracker.js +86 -39
  73. package/hooks/hooks.json +6 -1
  74. package/package.json +7 -7
  75. package/plugin.json +1 -1
  76. package/scripts/postinstall.js +10 -7
@@ -3,9 +3,9 @@
3
3
  * Forgen — SubagentStart/Stop Hook
4
4
  *
5
5
  * 에이전트 생성/종료 추적.
6
- * - 활성 에이전트 수 모니터링
7
- * - 에이전트 동시 실행 제한 (10개 초과 시 경고)
6
+ * - 활성 에이전트 수 모니터링 + 동시 실행 경고 (ADR-009 §4: 기본 16, workflow 면제)
8
7
  * - 에이전트 실행 이력 기록
8
+ * - ADR-009 §A: 상태 갱신을 file-lock 으로 보호 (동시 fanout lost-update 방지)
9
9
  */
10
10
  import * as fs from 'node:fs';
11
11
  import * as path from 'node:path';
@@ -14,23 +14,47 @@ import { isHookEnabled } from './hook-config.js';
14
14
  import { sanitizeId } from './shared/sanitize-id.js';
15
15
  import { atomicWriteJSON } from './shared/atomic-write.js';
16
16
  import { approve, approveWithWarning, failOpenWithTracking } from './shared/hook-response.js';
17
+ import { withFileLock } from './shared/file-lock.js';
17
18
  import { STATE_DIR } from '../core/paths.js';
18
- const MAX_CONCURRENT_AGENTS = 10;
19
+ /**
20
+ * 동시 에이전트 경고 임계값.
21
+ * ADR-009 §4: dynamic workflows 는 동시 16 까지 정상 사용하므로 기본값을 16 으로
22
+ * 올리고 env 로 조정 가능하게 한다. 과거 10 고정값은 workflow/team/swarm 실행마다
23
+ * 거짓 경고를 뱉었다.
24
+ */
25
+ export function maxConcurrentAgents() {
26
+ const env = Number(process.env.FORGEN_MAX_CONCURRENT_AGENTS);
27
+ return Number.isFinite(env) && env > 0 ? env : 16;
28
+ }
29
+ /**
30
+ * Claude Code 가 dynamic-workflow 내부 에이전트에 부여하는 agentType (probe 실측,
31
+ * 2026-05-29). 워크플로우 에이전트는 동시 16 이 정상이므로 동시성 경고에서 면제한다.
32
+ */
33
+ const WORKFLOW_AGENT_TYPE = 'workflow-subagent';
34
+ /**
35
+ * 동시성 경고를 띄울지 결정 (순수 — 테스트 대상).
36
+ * ADR-009 §4/§B: workflow-subagent 는 동시 16 이 정상이므로 면제. 그 외 에이전트가
37
+ * 임계값을 초과할 때만 경고.
38
+ */
39
+ export function shouldWarnConcurrency(agentType, activeCount, max) {
40
+ if (agentType === WORKFLOW_AGENT_TYPE)
41
+ return false;
42
+ return activeCount > max;
43
+ }
19
44
  const AGENT_GC_AGE_MS = 60 * 60 * 1000; // 1시간 이상 종료된 에이전트는 GC
20
45
  function getAgentsStatePath(sessionId) {
21
46
  return path.join(STATE_DIR, `active-agents-${sanitizeId(sessionId)}.json`);
22
47
  }
23
- function loadAgentsState(sessionId) {
48
+ function loadAgentsStateAt(statePath, sessionId) {
24
49
  try {
25
- const filePath = getAgentsStatePath(sessionId);
26
- if (fs.existsSync(filePath)) {
27
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
50
+ if (fs.existsSync(statePath)) {
51
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
28
52
  }
29
53
  }
30
- catch { /* agent state parse failure — starting fresh, active agent count history for this session is lost */ }
54
+ catch { /* parse failure — starting fresh, prior agent history for this session is lost */ }
31
55
  return { sessionId, agents: [] };
32
56
  }
33
- function saveAgentsState(state) {
57
+ function saveAgentsStateAt(statePath, state) {
34
58
  // GC: 1시간 이상 종료된 에이전트 제거
35
59
  const now = Date.now();
36
60
  state.agents = state.agents.filter(a => {
@@ -38,7 +62,41 @@ function saveAgentsState(state) {
38
62
  return true; // 활성 에이전트는 유지
39
63
  return now - new Date(a.stoppedAt).getTime() < AGENT_GC_AGE_MS;
40
64
  });
41
- atomicWriteJSON(getAgentsStatePath(state.sessionId), state);
65
+ atomicWriteJSON(statePath, state);
66
+ }
67
+ /**
68
+ * 에이전트 이벤트를 file-lock 아래에서 적용한다 (ADR-009 §A).
69
+ *
70
+ * 락이 없던 시절에는 동시 SubagentStart (워크플로우 fanout) 가 같은 active-agents
71
+ * 파일을 read-modify-write 하면서 lost-update 로 일부 에이전트가 누락됐다 (probe 에서
72
+ * 3개 중 1개 손실 관찰). 락 안에서 **fresh re-read** 후 mutate 해야 변경이 보존된다.
73
+ * staleMs 는 hook fn 이 짧으므로 5s 로 단축.
74
+ *
75
+ * statePath 는 테스트 주입용 (기본은 sessionId 파생 경로). 반환값은 start 후 활성
76
+ * 에이전트 수 (경고 판정에 사용; stop 은 0).
77
+ */
78
+ export async function recordAgentEvent(ev, statePath = getAgentsStatePath(ev.sessionId)) {
79
+ fs.mkdirSync(path.dirname(statePath), { recursive: true }); // lock 파일 생성 전 디렉토리 보장
80
+ let activeCount = 0;
81
+ await withFileLock(statePath, () => {
82
+ const state = loadAgentsStateAt(statePath, ev.sessionId); // 락 안에서 fresh re-read
83
+ if (ev.action === 'start') {
84
+ state.agents.push({
85
+ agentId: ev.agentId,
86
+ agentType: ev.agentType || undefined,
87
+ model: ev.model,
88
+ startedAt: new Date().toISOString(),
89
+ });
90
+ activeCount = state.agents.filter(a => !a.stoppedAt).length;
91
+ }
92
+ else if (ev.action === 'stop') {
93
+ const agent = state.agents.find(a => a.agentId === ev.agentId && !a.stoppedAt);
94
+ if (agent)
95
+ agent.stoppedAt = new Date().toISOString();
96
+ }
97
+ saveAgentsStateAt(statePath, state);
98
+ }, { staleMs: 5000 });
99
+ return { activeCount };
42
100
  }
43
101
  async function main() {
44
102
  const data = await readStdinJSON();
@@ -54,37 +112,26 @@ async function main() {
54
112
  }
55
113
  const sessionId = data.session_id ?? 'default';
56
114
  // 이벤트 타입은 argv[2] 또는 data 필드에서 판별
57
- const action = process.argv[2] ?? data.action ?? '';
115
+ const action = (process.argv[2] ?? data.action ?? '') === 'stop' ? 'stop' : 'start';
58
116
  const agentId = data.agent_id ?? data.agentId ?? `agent-${Date.now()}`;
59
117
  const agentType = data.agent_type ?? data.agentType ?? data.subagent_type ?? '';
60
- const state = loadAgentsState(sessionId);
61
- if (action === 'start') {
62
- const model = data.model ?? data.agentModel ?? undefined;
63
- state.agents.push({
64
- agentId,
65
- agentType: agentType || undefined,
66
- model,
67
- startedAt: new Date().toISOString(),
68
- });
69
- saveAgentsState(state);
70
- // 활성 에이전트 수 체크
71
- const activeCount = state.agents.filter(a => !a.stoppedAt).length;
72
- if (activeCount > MAX_CONCURRENT_AGENTS) {
73
- console.log(approveWithWarning(`<compound-tool-warning>\n[Forgen] ⚠ ${activeCount} active agents — too many concurrent executions. Watch resource usage.\n</compound-tool-warning>`));
74
- return;
75
- }
76
- }
77
- else if (action === 'stop') {
78
- // 해당 에이전트 종료 표시
79
- const agent = state.agents.find(a => a.agentId === agentId && !a.stoppedAt);
80
- if (agent) {
81
- agent.stoppedAt = new Date().toISOString();
82
- }
83
- saveAgentsState(state);
118
+ const model = data.model ?? data.agentModel ?? undefined;
119
+ // ADR-009 §A: 상태 갱신은 file-lock 아래에서. 락 실패/타임아웃은 .catch 의 fail-open
120
+ // 으로 흡수 (추적은 best-effort — 에이전트 실행 자체를 막지 않는다).
121
+ const { activeCount } = await recordAgentEvent({ sessionId, action, agentId, agentType, model });
122
+ // 동시성 경고 (락 밖). 면제/임계값 판정은 shouldWarnConcurrency (테스트 박제).
123
+ if (action === 'start' && shouldWarnConcurrency(agentType, activeCount, maxConcurrentAgents())) {
124
+ console.log(approveWithWarning(`<compound-tool-warning>\n[Forgen] ⚠ ${activeCount} active agents — too many concurrent executions. Watch resource usage.\n</compound-tool-warning>`));
125
+ return;
84
126
  }
85
127
  console.log(approve());
86
128
  }
87
- main().catch((e) => {
88
- process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
89
- console.log(failOpenWithTracking('subagent-tracker', e));
90
- });
129
+ // import.meta 가드: 직접 실행 시에만 main() — 테스트가 헬퍼를 import 할 때
130
+ // stdin 을 읽는 main 이 돌지 않도록 한다 (모듈 위생).
131
+ const isMain = import.meta.url === `file://${process.argv[1]}`;
132
+ if (isMain) {
133
+ main().catch((e) => {
134
+ process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
135
+ console.log(failOpenWithTracking('subagent-tracker', e));
136
+ });
137
+ }
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Forgen harness hooks (auto-generated, 21/21 active)",
2
+ "description": "Forgen harness hooks (auto-generated, 22/22 active)",
3
3
  "hooks": {
4
4
  "UserPromptSubmit": [
5
5
  {
@@ -175,6 +175,11 @@
175
175
  "type": "command",
176
176
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hooks/subagent-tracker.js\" \"stop\"",
177
177
  "timeout": 2
178
+ },
179
+ {
180
+ "type": "command",
181
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hooks/subagent-stop-guard.js\"",
182
+ "timeout": 10
178
183
  }
179
184
  ]
180
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooojin/forgen",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "preferGlobal": true,
5
5
  "main": "dist/lib.js",
6
6
  "types": "./dist/lib.d.ts",
@@ -29,7 +29,7 @@
29
29
  "format": "biome format --write src/",
30
30
  "postinstall": "node scripts/postinstall.js",
31
31
  "prepare": "npm run build",
32
- "prepublishOnly": "npm test",
32
+ "prepublishOnly": "npm run build && node scripts/prepack-hooks.cjs && npm test",
33
33
  "prepack": "npm run build && node scripts/prepack-hooks.cjs"
34
34
  },
35
35
  "author": "jang-ujin",
@@ -77,17 +77,17 @@
77
77
  "CONTRIBUTING.md"
78
78
  ],
79
79
  "devDependencies": {
80
- "@biomejs/biome": "^2.4.8",
80
+ "@biomejs/biome": "^2.4.16",
81
81
  "@types/js-yaml": "^4.0.9",
82
- "@types/node": "^25.5.0",
83
- "@vitest/coverage-v8": "^4.1.0",
82
+ "@types/node": "^25.9.1",
83
+ "@vitest/coverage-v8": "^4.1.7",
84
84
  "typescript": "^5.9.3",
85
- "vitest": "^4.0.18"
85
+ "vitest": "^4.1.7"
86
86
  },
87
87
  "dependencies": {
88
88
  "@modelcontextprotocol/sdk": "^1.28.0",
89
89
  "js-yaml": "^4.1.1",
90
- "zod": "^4.3.6"
90
+ "zod": "^4.4.3"
91
91
  },
92
92
  "peerDependencies": {
93
93
  "@anthropic-ai/claude-code": ">=2.0.0"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.10",
4
+ "version": "0.4.12",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",
@@ -1111,14 +1111,17 @@ async function main() {
1111
1111
  // First-run banner (신규 사용자 안내) — feat/codex-support P1-6
1112
1112
  if (!isExistingForgenUser) {
1113
1113
  console.log('');
1114
- console.log(' ╭─ forgen — multi-host support ─────────────────────────────╮');
1115
- console.log(' │ binaries installed: forgen, fgx, forgen-mcp │');
1114
+ console.log(' ╭─ forgen — setup checklist ────────────────────────────────╮');
1115
+ console.log(' │ │');
1116
+ console.log(' │ 1. Register on a host: │');
1117
+ console.log(' │ $ forgen install claude (or codex / both) │');
1118
+ console.log(' │ │');
1119
+ console.log(' │ 2. Complete onboarding (4 questions): │');
1120
+ console.log(' │ $ forgen │');
1121
+ console.log(' │ │');
1122
+ console.log(' │ 3. Verify setup: │');
1123
+ console.log(' │ $ forgen doctor --quick │');
1116
1124
  console.log(' │ │');
1117
- console.log(' │ Next: register forgen on a host (Claude or Codex): │');
1118
- console.log(' │ $ forgen install (interactive 3-choice) │');
1119
- console.log(' │ $ forgen install claude (Claude only) │');
1120
- console.log(' │ $ forgen install codex (Codex only) │');
1121
- console.log(' │ $ forgen install both (둘 다) │');
1122
1125
  console.log(' ╰────────────────────────────────────────────────────────────╯');
1123
1126
  console.log('');
1124
1127
  } else {