@wooojin/forgen 0.2.0 → 0.3.0

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 (158) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.ja.md +79 -14
  3. package/README.ko.md +100 -14
  4. package/README.md +124 -17
  5. package/README.zh.md +79 -14
  6. package/agents/analyst.md +48 -4
  7. package/agents/architect.md +39 -4
  8. package/agents/code-reviewer.md +107 -77
  9. package/agents/critic.md +47 -4
  10. package/agents/debugger.md +46 -4
  11. package/agents/designer.md +40 -4
  12. package/agents/executor.md +112 -30
  13. package/agents/explore.md +45 -5
  14. package/agents/git-master.md +48 -4
  15. package/agents/planner.md +121 -18
  16. package/agents/test-engineer.md +58 -4
  17. package/agents/verifier.md +92 -77
  18. package/commands/architecture-decision.md +127 -258
  19. package/commands/calibrate.md +225 -0
  20. package/commands/code-review.md +163 -178
  21. package/commands/compound.md +127 -68
  22. package/commands/deep-interview.md +273 -0
  23. package/commands/docker.md +68 -178
  24. package/commands/forge-loop.md +215 -0
  25. package/commands/learn.md +231 -0
  26. package/commands/retro.md +215 -0
  27. package/commands/ship.md +277 -0
  28. package/dist/cli.js +26 -9
  29. package/dist/core/auto-compound-runner.js +14 -0
  30. package/dist/core/config-injector.d.ts +2 -1
  31. package/dist/core/config-injector.js +2 -1
  32. package/dist/core/dashboard.d.ts +108 -0
  33. package/dist/core/dashboard.js +495 -0
  34. package/dist/core/doctor.js +151 -21
  35. package/dist/core/drift-score.d.ts +49 -0
  36. package/dist/core/drift-score.js +87 -0
  37. package/dist/core/harness.d.ts +6 -1
  38. package/dist/core/harness.js +75 -19
  39. package/dist/core/mcp-config.d.ts +2 -0
  40. package/dist/core/mcp-config.js +6 -1
  41. package/dist/core/paths.d.ts +6 -1
  42. package/dist/core/paths.js +18 -2
  43. package/dist/core/spawn.d.ts +3 -2
  44. package/dist/core/spawn.js +27 -8
  45. package/dist/core/types.d.ts +34 -0
  46. package/dist/engine/compound-export.d.ts +41 -0
  47. package/dist/engine/compound-export.js +169 -0
  48. package/dist/engine/compound-lifecycle.d.ts +4 -3
  49. package/dist/engine/compound-lifecycle.js +91 -46
  50. package/dist/engine/compound-loop.js +18 -0
  51. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  52. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  53. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  54. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  55. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  56. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  57. package/dist/engine/meta-learning/runner.d.ts +14 -0
  58. package/dist/engine/meta-learning/runner.js +90 -0
  59. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  60. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  61. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  62. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  63. package/dist/engine/meta-learning/types.d.ts +114 -0
  64. package/dist/engine/meta-learning/types.js +43 -0
  65. package/dist/engine/solution-format.d.ts +2 -2
  66. package/dist/engine/solution-format.js +249 -34
  67. package/dist/engine/solution-index.d.ts +1 -1
  68. package/dist/engine/solution-matcher.d.ts +30 -1
  69. package/dist/engine/solution-matcher.js +235 -45
  70. package/dist/fgx.js +12 -8
  71. package/dist/hooks/context-guard.d.ts +15 -0
  72. package/dist/hooks/context-guard.js +218 -56
  73. package/dist/hooks/db-guard.js +2 -2
  74. package/dist/hooks/hook-config.d.ts +27 -1
  75. package/dist/hooks/hook-config.js +72 -12
  76. package/dist/hooks/hooks-generator.d.ts +3 -0
  77. package/dist/hooks/hooks-generator.js +23 -6
  78. package/dist/hooks/intent-classifier.d.ts +0 -2
  79. package/dist/hooks/intent-classifier.js +32 -18
  80. package/dist/hooks/keyword-detector.js +126 -204
  81. package/dist/hooks/notepad-injector.js +2 -2
  82. package/dist/hooks/permission-handler.js +2 -2
  83. package/dist/hooks/post-tool-failure.js +12 -6
  84. package/dist/hooks/post-tool-handlers.d.ts +1 -1
  85. package/dist/hooks/post-tool-handlers.js +14 -11
  86. package/dist/hooks/post-tool-use.d.ts +11 -0
  87. package/dist/hooks/post-tool-use.js +184 -71
  88. package/dist/hooks/pre-compact.d.ts +11 -1
  89. package/dist/hooks/pre-compact.js +112 -37
  90. package/dist/hooks/pre-tool-use.js +86 -56
  91. package/dist/hooks/rate-limiter.js +3 -3
  92. package/dist/hooks/secret-filter.js +2 -2
  93. package/dist/hooks/session-recovery.js +256 -236
  94. package/dist/hooks/shared/hook-response.d.ts +4 -4
  95. package/dist/hooks/shared/hook-response.js +13 -24
  96. package/dist/hooks/shared/hook-timing.d.ts +15 -0
  97. package/dist/hooks/shared/hook-timing.js +64 -0
  98. package/dist/hooks/skill-injector.d.ts +4 -3
  99. package/dist/hooks/skill-injector.js +47 -16
  100. package/dist/hooks/slop-detector.js +3 -3
  101. package/dist/hooks/solution-injector.js +224 -197
  102. package/dist/hooks/subagent-tracker.js +2 -2
  103. package/dist/host/codex-adapter.d.ts +10 -0
  104. package/dist/host/codex-adapter.js +154 -0
  105. package/dist/mcp/solution-reader.d.ts +5 -5
  106. package/dist/mcp/solution-reader.js +34 -24
  107. package/dist/renderer/rule-renderer.js +9 -11
  108. package/dist/services/session.d.ts +19 -0
  109. package/dist/services/session.js +62 -0
  110. package/hooks/hooks.json +2 -2
  111. package/package.json +2 -1
  112. package/skills/architecture-decision/SKILL.md +113 -257
  113. package/skills/calibrate/SKILL.md +207 -0
  114. package/skills/code-review/SKILL.md +151 -178
  115. package/skills/compound/SKILL.md +126 -68
  116. package/skills/deep-interview/SKILL.md +266 -0
  117. package/skills/docker/SKILL.md +57 -179
  118. package/skills/forge-loop/SKILL.md +198 -0
  119. package/skills/learn/SKILL.md +216 -0
  120. package/skills/retro/SKILL.md +199 -0
  121. package/skills/ship/SKILL.md +259 -0
  122. package/agents/code-simplifier.md +0 -197
  123. package/agents/performance-reviewer.md +0 -172
  124. package/agents/qa-tester.md +0 -158
  125. package/agents/refactoring-expert.md +0 -168
  126. package/agents/scientist.md +0 -144
  127. package/agents/security-reviewer.md +0 -137
  128. package/agents/writer.md +0 -184
  129. package/commands/api-design.md +0 -268
  130. package/commands/ci-cd.md +0 -270
  131. package/commands/database.md +0 -263
  132. package/commands/debug-detective.md +0 -99
  133. package/commands/documentation.md +0 -276
  134. package/commands/ecomode.md +0 -51
  135. package/commands/frontend.md +0 -271
  136. package/commands/git-master.md +0 -90
  137. package/commands/incident-response.md +0 -292
  138. package/commands/migrate.md +0 -101
  139. package/commands/performance.md +0 -288
  140. package/commands/refactor.md +0 -105
  141. package/commands/security-review.md +0 -288
  142. package/commands/tdd.md +0 -183
  143. package/commands/testing-strategy.md +0 -265
  144. package/skills/api-design/SKILL.md +0 -262
  145. package/skills/ci-cd/SKILL.md +0 -264
  146. package/skills/database/SKILL.md +0 -257
  147. package/skills/debug-detective/SKILL.md +0 -95
  148. package/skills/documentation/SKILL.md +0 -270
  149. package/skills/ecomode/SKILL.md +0 -46
  150. package/skills/frontend/SKILL.md +0 -265
  151. package/skills/git-master/SKILL.md +0 -86
  152. package/skills/incident-response/SKILL.md +0 -286
  153. package/skills/migrate/SKILL.md +0 -96
  154. package/skills/performance/SKILL.md +0 -282
  155. package/skills/refactor/SKILL.md +0 -100
  156. package/skills/security-review/SKILL.md +0 -282
  157. package/skills/tdd/SKILL.md +0 -178
  158. package/skills/testing-strategy/SKILL.md +0 -260
@@ -1,9 +1,10 @@
1
+ import * as fs from 'node:fs';
1
2
  import * as path from 'node:path';
2
- import { ME_SOLUTIONS, PACKS_DIR } from '../core/paths.js';
3
- import { extractTags, expandCompoundTags, expandQueryBigrams } from './solution-format.js';
3
+ import { ME_SOLUTIONS, META_LEARNING_DIR, PACKS_DIR } from '../core/paths.js';
4
+ import { maskBlockedTokens } from './phrase-blocklist.js';
5
+ import { expandCompoundTags, expandQueryBigrams, extractTags } from './solution-format.js';
4
6
  import { getOrBuildIndex } from './solution-index.js';
5
7
  import { defaultNormalizer } from './term-normalizer.js';
6
- import { maskBlockedTokens } from './phrase-blocklist.js';
7
8
  // ── Synonym expansion (delegates to term-normalizer) ──
8
9
  //
9
10
  // The old `SYNONYM_MAP` + `expandTagsWithSynonyms` pair had two problems:
@@ -31,12 +32,106 @@ export function expandTagsWithSynonyms(tags) {
31
32
  return defaultNormalizer.normalizeTerms(tags);
32
33
  }
33
34
  // ── TF-IDF weighting for common tags ──
35
+ // ── Character bigram similarity (Dice coefficient) ──
36
+ /**
37
+ * Compute the Dice coefficient between two strings using character bigrams.
38
+ *
39
+ * Dice = 2 * |intersection| / (|A| + |B|)
40
+ *
41
+ * Both strings are lowercased and whitespace-stripped before bigram generation.
42
+ * Returns 0 for empty strings or single-character strings (no bigrams possible).
43
+ * Returns 1.0 for identical non-trivial strings.
44
+ *
45
+ * This is used as a lightweight fuzzy matching signal for borderline cases
46
+ * where the TF-IDF tag intersection produces a low score but the query and
47
+ * solution tags are character-similar (e.g., "database" vs "데이터베이스"
48
+ * won't match, but "database" vs "databse" will get a high score).
49
+ */
50
+ export function bigramSimilarity(a, b) {
51
+ const na = a.toLowerCase().replace(/\s+/g, '');
52
+ const nb = b.toLowerCase().replace(/\s+/g, '');
53
+ if (na.length < 2 || nb.length < 2)
54
+ return 0;
55
+ if (na === nb)
56
+ return 1.0;
57
+ const bigramsA = new Map();
58
+ for (let i = 0; i < na.length - 1; i++) {
59
+ const bg = na.slice(i, i + 2);
60
+ bigramsA.set(bg, (bigramsA.get(bg) ?? 0) + 1);
61
+ }
62
+ const bigramsB = new Map();
63
+ for (let i = 0; i < nb.length - 1; i++) {
64
+ const bg = nb.slice(i, i + 2);
65
+ bigramsB.set(bg, (bigramsB.get(bg) ?? 0) + 1);
66
+ }
67
+ let intersectionSize = 0;
68
+ for (const [bg, countA] of bigramsA) {
69
+ const countB = bigramsB.get(bg) ?? 0;
70
+ intersectionSize += Math.min(countA, countB);
71
+ }
72
+ const totalA = na.length - 1;
73
+ const totalB = nb.length - 1;
74
+ return (2 * intersectionSize) / (totalA + totalB);
75
+ }
76
+ // ── BM25-like scoring ──
77
+ /**
78
+ * Simplified BM25 score for a single query-document pair.
79
+ * Uses tag overlap with term frequency normalization.
80
+ * k1=1.2, b=0.75 (standard BM25 parameters).
81
+ */
82
+ export function bm25Score(queryTags, docTags, avgDocLength) {
83
+ const k1 = 1.2;
84
+ const b = 0.75;
85
+ const docLen = docTags.length;
86
+ if (docLen === 0 || queryTags.length === 0 || avgDocLength === 0)
87
+ return 0;
88
+ let score = 0;
89
+ for (const qt of queryTags) {
90
+ // Term frequency in document
91
+ const tf = docTags.filter((dt) => dt === qt || (dt.length > 3 && qt.length > 3 && (dt.includes(qt) || qt.includes(dt)))).length;
92
+ if (tf === 0)
93
+ continue;
94
+ // BM25 TF saturation
95
+ const numerator = tf * (k1 + 1);
96
+ const denominator = tf + k1 * (1 - b + b * (docLen / avgDocLength));
97
+ score += numerator / denominator;
98
+ }
99
+ // Normalize by query length
100
+ return score / queryTags.length;
101
+ }
34
102
  /** High-frequency tags that should be weighted lower */
35
103
  const COMMON_TAGS = new Set([
36
- 'typescript', 'ts', 'javascript', 'js', 'fix', 'update', 'add', 'change',
37
- 'file', 'code', 'function', 'import', 'export', 'error', 'type', 'string',
38
- 'number', 'object', 'array', 'return', 'const', 'class', 'module',
39
- '코드', '파일', '함수', '수정', '추가', '변경', '에러', '타입',
104
+ 'typescript',
105
+ 'ts',
106
+ 'javascript',
107
+ 'js',
108
+ 'fix',
109
+ 'update',
110
+ 'add',
111
+ 'change',
112
+ 'file',
113
+ 'code',
114
+ 'function',
115
+ 'import',
116
+ 'export',
117
+ 'error',
118
+ 'type',
119
+ 'string',
120
+ 'number',
121
+ 'object',
122
+ 'array',
123
+ 'return',
124
+ 'const',
125
+ 'class',
126
+ 'module',
127
+ '코드',
128
+ '파일',
129
+ '함수',
130
+ '수정',
131
+ '추가',
132
+ '변경',
133
+ '에러',
134
+ '타입',
40
135
  ]);
41
136
  /** Apply IDF-like weight: common tags get reduced weight */
42
137
  export function tagWeight(tag) {
@@ -47,7 +142,7 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
47
142
  // Legacy mode: substring matching for backwards compatibility.
48
143
  // Not a hot path — only hit by the (old) solution-matcher.test.ts cases.
49
144
  const promptTags = extractTags(promptOrTags);
50
- const intersection = keywordsOrTags.filter(kw => promptTags.some(pt => pt === kw || (pt.length > 3 && kw.length > 3 && (pt.startsWith(kw) || kw.startsWith(pt)))));
145
+ const intersection = keywordsOrTags.filter((kw) => promptTags.some((pt) => pt === kw || (pt.length > 3 && kw.length > 3 && (pt.startsWith(kw) || kw.startsWith(pt)))));
51
146
  return Math.min(1, intersection.length / Math.max(promptTags.length * 0.5, 1));
52
147
  }
53
148
  // v3 mode: tag matching with synonym expansion + TF-IDF weighting.
@@ -57,35 +152,86 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
57
152
  // the hot path pre-compute the expansion once per query and pass it via
58
153
  // `options.normalizedPromptTags`, so this function no longer repeats the
59
154
  // work per solution.
60
- const expandedPromptTags = options?.normalizedPromptTags
61
- ?? defaultNormalizer.normalizeTerms(promptOrTags);
155
+ const expandedPromptTags = options?.normalizedPromptTags ?? defaultNormalizer.normalizeTerms(promptOrTags);
62
156
  // R4-T1: when the caller supplies a compound-expanded solution tag set,
63
157
  // intersection and partial matching run against the expanded set (so
64
158
  // `api-key` matches `api`/`key` queries via the split parts), but the
65
159
  // Jaccard union denominator below still uses the RAW `keywordsOrTags`
66
160
  // for normalization stability.
67
161
  const matchTags = options?.solutionTagsExpanded ?? keywordsOrTags;
68
- const intersection = matchTags.filter(t => expandedPromptTags.includes(t));
162
+ const intersection = matchTags.filter((t) => expandedPromptTags.includes(t));
69
163
  // partial/substring matches for longer tags (>3 chars)
70
- const partialMatches = matchTags.filter(t => t.length > 3 && !intersection.includes(t)
71
- && expandedPromptTags.some(pt => pt.length > 3 && (pt.includes(t) || t.includes(pt))));
164
+ const partialMatches = matchTags.filter((t) => t.length > 3 &&
165
+ !intersection.includes(t) &&
166
+ expandedPromptTags.some((pt) => pt.length > 3 && (pt.includes(t) || t.includes(pt))));
72
167
  // Apply TF-IDF weighting: common tags count less
73
- const weightedMatched = intersection.reduce((sum, t) => sum + tagWeight(t), 0)
74
- + partialMatches.reduce((sum, t) => sum + tagWeight(t) * 0.5, 0);
75
- // 완화된 임계값: 가중 점수 0.5 이상이면 후보
76
- if (weightedMatched < 0.5)
168
+ const weightedMatched = intersection.reduce((sum, t) => sum + tagWeight(t), 0) +
169
+ partialMatches.reduce((sum, t) => sum + tagWeight(t) * 0.5, 0);
170
+ // ── Bigram similarity boost for borderline cases ──
171
+ //
172
+ // When the TF-IDF intersection score is below the match threshold (0.5),
173
+ // compute a character-bigram Dice coefficient between the query tags and
174
+ // the solution tags. If the best bigram similarity is high enough, blend
175
+ // it in at 20% weight (TF-IDF 80%, bigram 20%) to rescue fuzzy matches
176
+ // that the exact/substring intersection missed (e.g., typos, slight
177
+ // morphological variants).
178
+ //
179
+ // When TF-IDF score is already above threshold, the bigram boost is NOT
180
+ // applied — this preserves existing match quality and avoids disturbing
181
+ // already-good rankings. The bigram path is purely a rescue mechanism
182
+ // for borderline cases.
183
+ if (weightedMatched < 0.5) {
184
+ // Compute best bigram similarity across all (promptTag, solutionTag) pairs
185
+ let bestBigramScore = 0;
186
+ const bigramMatchedTags = [];
187
+ for (const st of matchTags) {
188
+ for (const pt of expandedPromptTags) {
189
+ const sim = bigramSimilarity(pt, st);
190
+ if (sim > bestBigramScore) {
191
+ bestBigramScore = sim;
192
+ }
193
+ // Track solution tags with meaningful bigram similarity (> 0.4)
194
+ if (sim > 0.4 && !bigramMatchedTags.includes(st)) {
195
+ bigramMatchedTags.push(st);
196
+ }
197
+ }
198
+ }
199
+ // Only rescue if the bigram signal is strong enough (> 0.4 threshold)
200
+ // to avoid noise from weakly similar strings
201
+ if (bestBigramScore > 0.4) {
202
+ const union = new Set([...promptOrTags, ...keywordsOrTags]).size;
203
+ const tfidfScore = weightedMatched / Math.max(union, 1);
204
+ const blendedScore = tfidfScore * 0.8 + bestBigramScore * 0.2;
205
+ return {
206
+ relevance: blendedScore * (confidence ?? 1),
207
+ matchedTags: [
208
+ ...intersection,
209
+ ...partialMatches,
210
+ ...bigramMatchedTags.filter((t) => !intersection.includes(t) && !partialMatches.includes(t)),
211
+ ],
212
+ };
213
+ }
77
214
  return { relevance: 0, matchedTags: [] };
78
- // Jaccard-like: weighted matched / union.
79
- // Union uses RAW promptTags and RAW solutionTags not the expanded set —
80
- // so that the denominator semantics are unchanged from pre-T2 behaviour.
81
- // This is intentional: expanding both sides of the Jaccard would
82
- // asymmetrically inflate recall and silently shift all baseline metrics.
83
- // R4-T1 explicitly preserves this: `keywordsOrTags` is the raw solution
84
- // tag list, not the compound-expanded `matchTags` used above.
215
+ }
216
+ // Ensemble: TF-IDF (Jaccard) 0.5 + BM25 0.3 + bigram 0.2
85
217
  const union = new Set([...promptOrTags, ...keywordsOrTags]).size;
86
- const tagScore = weightedMatched / Math.max(union, 1);
218
+ const tfidfScore = weightedMatched / Math.max(union, 1);
219
+ // BM25 component: average doc length defaults to 6 tags (typical solution)
220
+ const avgDocLen = options?.avgDocLength ?? 6;
221
+ const bm25 = bm25Score(promptOrTags, keywordsOrTags, avgDocLen);
222
+ // Bigram component (mild boost for partial string matches)
223
+ let bigramBoost = 0;
224
+ for (const st of matchTags) {
225
+ for (const pt of expandedPromptTags) {
226
+ const sim = bigramSimilarity(pt, st);
227
+ if (sim > bigramBoost)
228
+ bigramBoost = sim;
229
+ }
230
+ }
231
+ const w = options?.ensembleWeights ?? { tfidf: 0.5, bm25: 0.3, bigram: 0.2 };
232
+ const ensembleScore = tfidfScore * w.tfidf + bm25 * w.bm25 + bigramBoost * w.bigram;
87
233
  return {
88
- relevance: tagScore * (confidence ?? 1),
234
+ relevance: ensembleScore * (confidence ?? 1),
89
235
  matchedTags: [...intersection, ...partialMatches],
90
236
  };
91
237
  }
@@ -161,8 +307,8 @@ export function shouldRejectByR4T3Rules(promptTags, matchedTags) {
161
307
  // Rule B
162
308
  if (matchedTags.length === 1) {
163
309
  const tag = matchedTags[0];
164
- const literalHit = promptTags.includes(tag)
165
- || promptTags.some(pt => {
310
+ const literalHit = promptTags.includes(tag) ||
311
+ promptTags.some((pt) => {
166
312
  if (pt.length <= 3 || tag.length <= 3)
167
313
  return false;
168
314
  if (pt.includes(tag) || tag.includes(pt))
@@ -197,7 +343,7 @@ export function shouldRejectByR4T3Rules(promptTags, matchedTags) {
197
343
  * `matchSolutions` behaviour (both scopes could rank). Callers that want
198
344
  * first-wins scope precedence must dedupe on their side.
199
345
  */
200
- function rankCandidates(promptTags, promptLower, solutions) {
346
+ function rankCandidates(promptTags, promptLower, solutions, ensembleWeights) {
201
347
  // T2: normalize prompt tags ONCE per query (not once per solution).
202
348
  // Pre-T2 this expansion happened inside calculateRelevance and was
203
349
  // repeated N times for N solutions — the plan's primary hot-path win.
@@ -232,7 +378,7 @@ function rankCandidates(promptTags, promptLower, solutions) {
232
378
  const promptTagsWithBigrams = expandQueryBigrams(maskedPromptTags);
233
379
  const normalizedPromptTags = defaultNormalizer.normalizeTerms(promptTagsWithBigrams);
234
380
  return solutions
235
- .map(sol => {
381
+ .map((sol) => {
236
382
  // R4-T1: solution-side compound-tag expansion. `api-key` becomes
237
383
  // {api-key, api, key} so a query token `api` (from "api keys") hits
238
384
  // it directly. Computed per solution because each sol.tags is
@@ -245,7 +391,11 @@ function rankCandidates(promptTags, promptLower, solutions) {
245
391
  // step (intersection/partialMatches) already uses the masked set
246
392
  // via `normalizedPromptTags` — the union must match for score
247
393
  // semantics to stay consistent.
248
- const result = calculateRelevance(maskedPromptTags, sol.tags, sol.confidence, { normalizedPromptTags, solutionTagsExpanded: solTagsExpanded });
394
+ const result = calculateRelevance(maskedPromptTags, sol.tags, sol.confidence, {
395
+ normalizedPromptTags,
396
+ solutionTagsExpanded: solTagsExpanded,
397
+ ensembleWeights,
398
+ });
249
399
  // Compute identifier boost FIRST — independent of tag scoring so
250
400
  // R4-T3's tag-evidence precision rules below cannot silently drop
251
401
  // a candidate that has strong identifier-level evidence.
@@ -272,9 +422,9 @@ function rankCandidates(promptTags, promptLower, solutions) {
272
422
  // the `matchedTags.length + matchedIdentifiers.length >= 1` filter.
273
423
  let tagRelevance = result.relevance;
274
424
  let tagMatches = result.matchedTags;
275
- if (matchedIdentifiers.length === 0
276
- && tagMatches.length > 0
277
- && shouldRejectByR4T3Rules(maskedPromptTags, tagMatches)) {
425
+ if (matchedIdentifiers.length === 0 &&
426
+ tagMatches.length > 0 &&
427
+ shouldRejectByR4T3Rules(maskedPromptTags, tagMatches)) {
278
428
  tagRelevance = 0;
279
429
  tagMatches = [];
280
430
  }
@@ -285,7 +435,7 @@ function rankCandidates(promptTags, promptLower, solutions) {
285
435
  matchedIdentifiers,
286
436
  };
287
437
  })
288
- .filter(c => c.matchedTags.length + c.matchedIdentifiers.length >= 1)
438
+ .filter((c) => c.matchedTags.length + c.matchedIdentifiers.length >= 1)
289
439
  .sort((a, b) => b.relevance - a.relevance)
290
440
  .slice(0, 5);
291
441
  }
@@ -562,7 +712,7 @@ function computeBucketMetrics(queries, solutions) {
562
712
  */
563
713
  export function evaluateQuery(query, solutions) {
564
714
  const promptTags = extractTags(query);
565
- return rankCandidates(promptTags, query.toLowerCase(), solutions).map(c => ({
715
+ return rankCandidates(promptTags, query.toLowerCase(), solutions).map((c) => ({
566
716
  name: c.solution.name,
567
717
  relevance: c.relevance,
568
718
  matchedTags: c.matchedTags,
@@ -588,13 +738,16 @@ export function evaluateSolutionMatcher(fixture) {
588
738
  // doesn't drown a small paraphrase bucket but also a single-query bucket
589
739
  // doesn't dominate.
590
740
  const recallAt5 = combinedTotal > 0
591
- ? (positiveM.recallAt5 * positiveM.total + paraphraseM.recallAt5 * paraphraseM.total) / combinedTotal
741
+ ? (positiveM.recallAt5 * positiveM.total + paraphraseM.recallAt5 * paraphraseM.total) /
742
+ combinedTotal
592
743
  : 0;
593
744
  const mrrAt5 = combinedTotal > 0
594
- ? (positiveM.mrrAt5 * positiveM.total + paraphraseM.mrrAt5 * paraphraseM.total) / combinedTotal
745
+ ? (positiveM.mrrAt5 * positiveM.total + paraphraseM.mrrAt5 * paraphraseM.total) /
746
+ combinedTotal
595
747
  : 0;
596
748
  const noResultRate = combinedTotal > 0
597
- ? (positiveM.noResultRate * positiveM.total + paraphraseM.noResultRate * paraphraseM.total) / combinedTotal
749
+ ? (positiveM.noResultRate * positiveM.total + paraphraseM.noResultRate * paraphraseM.total) /
750
+ combinedTotal
598
751
  : 0;
599
752
  let negAnyResult = 0;
600
753
  for (const q of fixture.negative) {
@@ -620,27 +773,64 @@ export function evaluateSolutionMatcher(fixture) {
620
773
  },
621
774
  };
622
775
  }
776
+ // ── Meta-learning: dynamic ensemble weights ──
777
+ let _cachedWeights;
778
+ let _weightsCacheTime = 0;
779
+ const WEIGHTS_CACHE_TTL = 60_000; // 1 minute cache
780
+ /**
781
+ * Load tuned matcher weights from meta-learning state.
782
+ * Returns undefined (use defaults) if no tuned weights exist.
783
+ * Cached for 1 minute to avoid re-reading per matchSolutions call.
784
+ */
785
+ function loadTunedMatcherWeights() {
786
+ const now = Date.now();
787
+ if (_cachedWeights !== undefined && now - _weightsCacheTime < WEIGHTS_CACHE_TTL) {
788
+ return _cachedWeights ?? undefined;
789
+ }
790
+ try {
791
+ const weightsPath = path.join(META_LEARNING_DIR, 'matcher-weights.json');
792
+ if (!fs.existsSync(weightsPath)) {
793
+ _cachedWeights = null;
794
+ _weightsCacheTime = now;
795
+ return undefined;
796
+ }
797
+ const data = JSON.parse(fs.readFileSync(weightsPath, 'utf-8'));
798
+ if (typeof data.tfidf === 'number' &&
799
+ typeof data.bm25 === 'number' &&
800
+ typeof data.bigram === 'number') {
801
+ _cachedWeights = { tfidf: data.tfidf, bm25: data.bm25, bigram: data.bigram };
802
+ _weightsCacheTime = now;
803
+ return _cachedWeights;
804
+ }
805
+ }
806
+ catch {
807
+ /* fail-open: use defaults */
808
+ }
809
+ _cachedWeights = null;
810
+ _weightsCacheTime = now;
811
+ return undefined;
812
+ }
623
813
  export function matchSolutions(prompt, scope, cwd) {
624
814
  // Build solution dirs for index cache
625
- const dirs = [
626
- { dir: ME_SOLUTIONS, scope: 'me' },
627
- ];
815
+ const dirs = [{ dir: ME_SOLUTIONS, scope: 'me' }];
628
816
  if (scope.team) {
629
817
  dirs.push({ dir: path.join(PACKS_DIR, scope.team.name, 'solutions'), scope: 'team' });
630
818
  }
631
819
  dirs.push({ dir: path.join(cwd, '.compound', 'solutions'), scope: 'project' });
632
820
  // Use cached index (rebuilt only when dirs change)
633
821
  const index = getOrBuildIndex(dirs);
634
- const allSolutions = index.entries.map(e => ({ ...e }));
822
+ const allSolutions = index.entries.map((e) => ({ ...e }));
635
823
  const promptTags = extractTags(prompt);
636
824
  const promptLower = prompt.toLowerCase();
825
+ // Meta-learning: load tuned weights if available
826
+ const tunedWeights = loadTunedMatcherWeights();
637
827
  // Delegate to shared ranking core. `rankCandidates` is generic so each
638
828
  // ranked candidate carries the original `LoadedSolution` reference — no
639
829
  // name-based re-lookup, so two scopes sharing a name (e.g. me/foo and
640
830
  // project/foo) can both appear in the result without a Map last-wins
641
831
  // scope-precedence bug.
642
- const ranked = rankCandidates(promptTags, promptLower, allSolutions);
643
- return ranked.map(c => ({
832
+ const ranked = rankCandidates(promptTags, promptLower, allSolutions, tunedWeights);
833
+ return ranked.map((c) => ({
644
834
  name: c.solution.name,
645
835
  path: c.solution.filePath,
646
836
  scope: c.solution.scope,
package/dist/fgx.js CHANGED
@@ -3,14 +3,17 @@
3
3
  * fgx — forgen --dangerously-skip-permissions 의 단축 명령
4
4
  * 모든 인자를 그대로 전달하되, --dangerously-skip-permissions 를 자동 주입
5
5
  */
6
+ import { resolveLaunchContext } from './services/session.js';
7
+ import { prepareHarness, isFirstRun } from './core/harness.js';
8
+ import { spawnClaude } from './core/spawn.js';
6
9
  const args = process.argv.slice(2);
7
10
  // 이미 포함되어 있으면 중복 추가하지 않음
8
- if (!args.includes('--dangerously-skip-permissions')) {
9
- args.unshift('--dangerously-skip-permissions');
11
+ const launchContext = resolveLaunchContext(args);
12
+ const runtime = launchContext.runtime;
13
+ const launchArgs = [...launchContext.args];
14
+ if (!launchArgs.includes('--dangerously-skip-permissions')) {
15
+ launchArgs.unshift('--dangerously-skip-permissions');
10
16
  }
11
- // cli.ts 의 main 로직을 재사용
12
- import { prepareHarness, isFirstRun } from './core/harness.js';
13
- import { spawnClaude } from './core/spawn.js';
14
17
  async function main() {
15
18
  // Security warning — fgx bypasses all Claude Code permission checks
16
19
  console.warn('\n ⚠ fgx: ALL permission checks are disabled (--dangerously-skip-permissions)');
@@ -23,7 +26,7 @@ async function main() {
23
26
  console.log(' Creating ~/.forgen/ directory and default philosophy.');
24
27
  console.log(' Run `forgen onboarding` afterwards to complete personalization.\n');
25
28
  }
26
- const context = await prepareHarness(process.cwd());
29
+ const context = await prepareHarness(process.cwd(), { runtime });
27
30
  if (firstRun) {
28
31
  console.log(' [Done] Initial setup complete.\n');
29
32
  }
@@ -33,8 +36,9 @@ async function main() {
33
36
  console.log(`[forgen] Trust: ${v1.session.effective_trust_policy}`);
34
37
  }
35
38
  console.log('[forgen] Mode: dangerously-skip-permissions');
36
- console.log('[forgen] Starting Claude Code...\n');
37
- await spawnClaude(args, context);
39
+ const runtimeLabel = runtime === 'codex' ? 'Codex' : 'Claude';
40
+ console.log(`[forgen] Starting ${runtimeLabel}...\n`);
41
+ await spawnClaude(launchArgs, context, runtime);
38
42
  }
39
43
  main().catch((err) => {
40
44
  console.error('[forgen] Error:', err instanceof Error ? err.message : err);
@@ -19,6 +19,21 @@ export declare function shouldWarn(contextPercent: {
19
19
  charsThreshold?: number;
20
20
  cooldownMs?: number;
21
21
  }): boolean;
22
+ /** auto-compact 트리거 여부 판정 (순수 함수) */
23
+ export declare function shouldAutoCompact(state: {
24
+ totalChars: number;
25
+ lastAutoCompactAt: number;
26
+ }, thresholds?: {
27
+ charsThreshold?: number;
28
+ cooldownMs?: number;
29
+ }): boolean;
30
+ /** auto-compact 지시 메시지 생성 (순수 함수) */
31
+ export declare function buildAutoCompactMessage(totalChars: number): string;
22
32
  /** 경고 메시지 생성 (순수 함수) */
23
33
  export declare function buildContextWarningMessage(promptCount: number, totalChars: number): string;
24
34
  export declare function main(): Promise<void>;
35
+ /**
36
+ * forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
37
+ * OMC의 persistent-mode.cjs 패턴 참고.
38
+ */
39
+ export declare function checkForgeLoopActive(): string | null;