mio-agent-runtime 0.13.4 → 0.14.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.
- package/README.md +44 -14
- package/adapters/codex.js +121 -121
- package/adapters/hermes.js +182 -176
- package/adapters/opencode.js +203 -202
- package/adapters/workbuddy.js +197 -197
- package/bin/mio.js +223 -15
- package/observe/observer.js +1876 -1867
- package/package.json +5 -3
- package/scripts/link-workspace.cjs +52 -0
- package/server/creativity-engine.js +279 -114
- package/server/creativity-sources.js +186 -0
- package/server/evolution-cutover.js +5 -2
- package/server/idea-generate.js +136 -0
- package/server/insight-store.js +37 -11
- package/server/llm-client.js +29 -8
- package/server/mio-intelligence-mcp/README.md +173 -173
- package/server/mio-intelligence-mcp/index.js +103 -6
- package/server/mio-intelligence-mcp/package.json +17 -17
- package/server/mio-intelligence-mcp/phase0.js +365 -365
- package/server/observer-store.js +12 -11
- package/server/task-store.js +475 -475
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"!server/mio-intelligence-mcp/__tests__"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
+
"pretest": "node scripts/link-workspace.cjs",
|
|
20
21
|
"check": "node scripts/check-syntax.cjs",
|
|
21
22
|
"test": "node --test \"__tests__/*.test.js\" \"server/mio-intelligence-mcp/__tests__/*.test.js\"",
|
|
22
23
|
"prepack": "npm run check",
|
|
@@ -33,8 +34,9 @@
|
|
|
33
34
|
"@akemi-mio/evolution-strategy": "^0.1.0",
|
|
34
35
|
"@akemi-mio/evolution-safety": "^0.1.0",
|
|
35
36
|
"@akemi-mio/evolution-scheduler": "^0.1.0",
|
|
36
|
-
"@akemi-mio/observer": "^0.1.
|
|
37
|
-
"@akemi-mio/insight": "^0.1.
|
|
37
|
+
"@akemi-mio/observer": "^0.1.4",
|
|
38
|
+
"@akemi-mio/insight": "^0.1.1",
|
|
39
|
+
"@akemi-mio/creativity": "^0.1.0"
|
|
38
40
|
},
|
|
39
41
|
"main": "index.js",
|
|
40
42
|
"directories": {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Creates the node_modules junctions mio-cli needs to require the workspace
|
|
5
|
+
// packages it depends on at runtime (@akemi-mio/insight etc. already exist as
|
|
6
|
+
// junctions in a normal checkout; a fresh clone has none). Idempotent: an
|
|
7
|
+
// existing link (or directory) is left alone. Windows junctions, because this
|
|
8
|
+
// checkout lives on NTFS and junctions need no admin rights.
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
|
|
13
|
+
const REPO = path.resolve(__dirname, '..', '..', '..')
|
|
14
|
+
const links = [
|
|
15
|
+
{
|
|
16
|
+
at: path.join(REPO, 'packages', 'mio-cli', 'node_modules', '@akemi-mio', 'creativity'),
|
|
17
|
+
target: path.join(REPO, 'packages', 'creativity'),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
at: path.join(REPO, 'packages', 'creativity', 'node_modules', '@akemi-mio', 'core'),
|
|
21
|
+
target: path.join(REPO, 'packages', 'core'),
|
|
22
|
+
},
|
|
23
|
+
// Root-level links: the new tests under tests/main/creativity/ import
|
|
24
|
+
// '@akemi-mio/creativity/*' from the repo root, and the lockfile's
|
|
25
|
+
// link:true entries are not materialized on disk (ground rule 4 forbids
|
|
26
|
+
// the `npm install` that would create them).
|
|
27
|
+
{
|
|
28
|
+
at: path.join(REPO, 'node_modules', '@akemi-mio', 'creativity'),
|
|
29
|
+
target: path.join(REPO, 'packages', 'creativity'),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
at: path.join(REPO, 'node_modules', '@akemi-mio', 'core'),
|
|
33
|
+
target: path.join(REPO, 'packages', 'core'),
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for (const { at, target } of links) {
|
|
38
|
+
let exists = false
|
|
39
|
+
try {
|
|
40
|
+
fs.lstatSync(at)
|
|
41
|
+
exists = true
|
|
42
|
+
} catch (_) {
|
|
43
|
+
exists = false
|
|
44
|
+
}
|
|
45
|
+
if (exists) {
|
|
46
|
+
console.log(`exists ${at}`)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
fs.mkdirSync(path.dirname(at), { recursive: true })
|
|
50
|
+
fs.symlinkSync(target, at, 'junction')
|
|
51
|
+
console.log(`linked ${at} -> ${target}`)
|
|
52
|
+
}
|
|
@@ -3,6 +3,37 @@
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const crypto = require('crypto')
|
|
6
|
+
const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
|
|
7
|
+
|
|
8
|
+
// Cache validity key for a JSONL file: a (size, mtimeMs) pair, the same
|
|
9
|
+
// guard server/insight-store.js uses (commit 0ba7fa8). Every getter below used
|
|
10
|
+
// to latch its parsed array forever, so a long-running MCP process answered
|
|
11
|
+
// `mio.creativity.list` from the copy it loaded at startup and a
|
|
12
|
+
// `mio creativity generate` in another process stayed invisible until restart.
|
|
13
|
+
function fileSignature(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
const st = fs.statSync(filePath)
|
|
16
|
+
return `${st.size}:${st.mtimeMs}`
|
|
17
|
+
} catch (_) {
|
|
18
|
+
return 'missing'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// One clamp for every LLM score: coerce, reject NaN/Infinity (a model that
|
|
23
|
+
// returns "high" used to store NaN and poison the sum gate), then bound to
|
|
24
|
+
// 0-100. Missing values fall back so old records keep a neutral score.
|
|
25
|
+
function clamp100(value, fallback = 50) {
|
|
26
|
+
const n = Number(value)
|
|
27
|
+
if (!Number.isFinite(n)) return fallback
|
|
28
|
+
return Math.min(100, Math.max(0, n))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ISO timestamp for provenance: accepts epoch ms or date strings, returns
|
|
32
|
+
// undefined when the value is missing or unparsable.
|
|
33
|
+
function isoOf(value) {
|
|
34
|
+
const t = typeof value === 'number' ? value : Date.parse(value || '')
|
|
35
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : undefined
|
|
36
|
+
}
|
|
6
37
|
|
|
7
38
|
// ═══════════════════════════════════════════════
|
|
8
39
|
// JSONL Store — ideas, combos, experiments
|
|
@@ -17,6 +48,9 @@ class CreativityStore {
|
|
|
17
48
|
this._hypotheses = null
|
|
18
49
|
this._combos = null
|
|
19
50
|
this._experiments = null
|
|
51
|
+
this._hypothesesSig = null
|
|
52
|
+
this._combosSig = null
|
|
53
|
+
this._experimentsSig = null
|
|
20
54
|
}
|
|
21
55
|
|
|
22
56
|
_ensureDir() {
|
|
@@ -42,8 +76,10 @@ class CreativityStore {
|
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
getHypotheses(opts = {}) {
|
|
45
|
-
|
|
79
|
+
const sig = fileSignature(this.hypothesesPath)
|
|
80
|
+
if (this._hypotheses === null || this._hypothesesSig !== sig) {
|
|
46
81
|
this._hypotheses = this._readJsonl(this.hypothesesPath)
|
|
82
|
+
this._hypothesesSig = sig
|
|
47
83
|
}
|
|
48
84
|
let list = this._hypotheses
|
|
49
85
|
if (opts.status) list = list.filter(h => h.status === opts.status)
|
|
@@ -53,6 +89,7 @@ class CreativityStore {
|
|
|
53
89
|
|
|
54
90
|
addHypothesis(h) {
|
|
55
91
|
this._appendJsonl(this.hypothesesPath, h)
|
|
92
|
+
this._hypothesesSig = fileSignature(this.hypothesesPath)
|
|
56
93
|
if (this._hypotheses) this._hypotheses.push(h)
|
|
57
94
|
}
|
|
58
95
|
|
|
@@ -63,30 +100,37 @@ class CreativityStore {
|
|
|
63
100
|
Object.assign(all[idx], patch)
|
|
64
101
|
this._hypotheses = all
|
|
65
102
|
this._rewriteFile(this.hypothesesPath, all)
|
|
103
|
+
this._hypothesesSig = fileSignature(this.hypothesesPath)
|
|
66
104
|
return true
|
|
67
105
|
}
|
|
68
106
|
|
|
69
107
|
getCombos(limit) {
|
|
70
|
-
|
|
108
|
+
const sig = fileSignature(this.combosPath)
|
|
109
|
+
if (this._combos === null || this._combosSig !== sig) {
|
|
71
110
|
this._combos = this._readJsonl(this.combosPath)
|
|
111
|
+
this._combosSig = sig
|
|
72
112
|
}
|
|
73
113
|
return limit ? this._combos.slice(-limit) : this._combos
|
|
74
114
|
}
|
|
75
115
|
|
|
76
116
|
addCombo(c) {
|
|
77
117
|
this._appendJsonl(this.combosPath, c)
|
|
118
|
+
this._combosSig = fileSignature(this.combosPath)
|
|
78
119
|
if (this._combos) this._combos.push(c)
|
|
79
120
|
}
|
|
80
121
|
|
|
81
122
|
getExperiments() {
|
|
82
|
-
|
|
123
|
+
const sig = fileSignature(this.experimentsPath)
|
|
124
|
+
if (this._experiments === null || this._experimentsSig !== sig) {
|
|
83
125
|
this._experiments = this._readJsonl(this.experimentsPath)
|
|
126
|
+
this._experimentsSig = sig
|
|
84
127
|
}
|
|
85
128
|
return this._experiments
|
|
86
129
|
}
|
|
87
130
|
|
|
88
131
|
addExperiment(e) {
|
|
89
132
|
this._appendJsonl(this.experimentsPath, e)
|
|
133
|
+
this._experimentsSig = fileSignature(this.experimentsPath)
|
|
90
134
|
if (this._experiments) this._experiments.push(e)
|
|
91
135
|
}
|
|
92
136
|
|
|
@@ -138,48 +182,61 @@ function mixConcepts(sources, exploredPairs, rng) {
|
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
// ═══════════════════════════════════════════════
|
|
141
|
-
//
|
|
185
|
+
// Creativity Engine — main orchestrator
|
|
142
186
|
// ═══════════════════════════════════════════════
|
|
143
187
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
188
|
+
// Concept sources drawn from stored insights, backing
|
|
189
|
+
// `mio.creativity.generate { fromInsights: true }` / `mio creativity generate
|
|
190
|
+
// --from-insights`. The *caller* supplies the insight records because only the
|
|
191
|
+
// entry point knows which store is authoritative (MIO_HOME for the CLI, the
|
|
192
|
+
// MCP server's MIO_DATA_DIR); this only maps them onto the { name, content,
|
|
193
|
+
// type, weight } shape the concept mixer pairs up. Highest score first, capped
|
|
194
|
+
// so one busy insight run cannot turn into dozens of LLM calls.
|
|
195
|
+
function sourcesFromInsights(insights, limit = 10) {
|
|
196
|
+
return (Array.isArray(insights) ? insights : [])
|
|
197
|
+
.filter(i => i && (i.title || i.description || i.content))
|
|
198
|
+
.slice()
|
|
199
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
200
|
+
.slice(0, limit)
|
|
201
|
+
.map(i => ({
|
|
202
|
+
name: String(i.title || i.id || 'insight').replace(/\s+/g, ' ').slice(0, 60),
|
|
203
|
+
content: [
|
|
204
|
+
i.description || i.content || i.title || '',
|
|
205
|
+
Array.isArray(i.evidence) && i.evidence.length > 0 ? `Evidence: ${i.evidence.join('; ')}` : '',
|
|
206
|
+
].filter(Boolean).join('\n'),
|
|
207
|
+
type: 'insight',
|
|
208
|
+
// > 0.7: IdeaGenerator.applyTemperature keeps any weight>0.7 source unconditionally.
|
|
209
|
+
weight: 0.78,
|
|
210
|
+
origin: `insight:${i.id || i.title || 'unknown'}`,
|
|
211
|
+
...(i.id ? { originId: i.id } : {}),
|
|
212
|
+
...(isoOf(i.createdAt) ? { timestamp: isoOf(i.createdAt) } : {}),
|
|
213
|
+
}))
|
|
151
214
|
}
|
|
152
215
|
|
|
153
|
-
|
|
154
|
-
if (a.size === 0 && b.size === 0) return 1
|
|
155
|
-
let inter = 0
|
|
156
|
-
for (const x of a) if (b.has(x)) inter++
|
|
157
|
-
return inter / (a.size + b.size - inter)
|
|
158
|
-
}
|
|
216
|
+
const STRATEGY_CYCLE = ['explore', 'signal', 'stable']
|
|
159
217
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
218
|
+
// One row shape for every list variant — the fields JSON/CLI consumers already
|
|
219
|
+
// depend on. `noveltyScore` is added only by sort=novelty (relative ranking),
|
|
220
|
+
// never confused with the quality sum `score`.
|
|
221
|
+
function listRow(h) {
|
|
222
|
+
return {
|
|
223
|
+
id: h.id,
|
|
224
|
+
title: h.title,
|
|
225
|
+
idea: h.idea,
|
|
226
|
+
status: h.status,
|
|
227
|
+
novelty: h.novelty,
|
|
228
|
+
feasibility: h.feasibility,
|
|
229
|
+
impact: h.impact,
|
|
230
|
+
...(h.logic !== undefined ? { logic: h.logic } : {}),
|
|
231
|
+
score: h.novelty + h.feasibility + h.impact,
|
|
232
|
+
sourceLabels: h.sourceLabels,
|
|
233
|
+
createdAt: h.createdAt,
|
|
234
|
+
fermentedAt: h.fermentedAt,
|
|
235
|
+
fermentCount: h.fermentCount,
|
|
236
|
+
...(h.mergedInto !== undefined ? { mergedInto: h.mergedInto } : {}),
|
|
167
237
|
}
|
|
168
|
-
for (const r of rejected) {
|
|
169
|
-
const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
|
|
170
|
-
if (sim > 0.6) {
|
|
171
|
-
return { shouldReject: true, rejectReason: `matches rejected: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return { shouldReject: false, adjustedNovelty: Math.min(100, idea.novelty + 5) }
|
|
175
238
|
}
|
|
176
239
|
|
|
177
|
-
// ═══════════════════════════════════════════════
|
|
178
|
-
// Creativity Engine — main orchestrator
|
|
179
|
-
// ═══════════════════════════════════════════════
|
|
180
|
-
|
|
181
|
-
const STRATEGY_CYCLE = ['explore', 'signal', 'stable']
|
|
182
|
-
|
|
183
240
|
class CreativityEngine {
|
|
184
241
|
constructor(dataDir, chatJson) {
|
|
185
242
|
this.store = new CreativityStore(dataDir)
|
|
@@ -221,56 +278,84 @@ class CreativityEngine {
|
|
|
221
278
|
|
|
222
279
|
// Generate hypotheses via LLM
|
|
223
280
|
const ideas = []
|
|
281
|
+
// A failed pair used to vanish: `catch {}` swallowed the exception and a
|
|
282
|
+
// chatJson `{ error }` payload fell through the `if (result.data)` guard,
|
|
283
|
+
// so the caller saw `ideas: []` with no explanation of whether the LLM was
|
|
284
|
+
// down, the JSON was unparseable, or every pair really was a duplicate.
|
|
285
|
+
const errors = []
|
|
224
286
|
for (const [a, b] of pairs) {
|
|
225
287
|
const prompt = this._buildGenerationPrompt(a, b, strat, recent.slice(-10), rejected.slice(-5))
|
|
226
288
|
try {
|
|
227
289
|
const result = await this.chatJson(prompt, {
|
|
228
|
-
system:
|
|
290
|
+
system: [
|
|
291
|
+
'You are a creative hypothesis generator. Generate ONE novel hypothesis by combining the given concepts.',
|
|
292
|
+
'Reasoning rules:',
|
|
293
|
+
'1. First state the synergy mechanism: WHY these two concepts combine (what shared structure or complementary gap), not just THAT they pair.',
|
|
294
|
+
'2. Then give a causal chain: premise -> mechanism -> outcome. Each step must follow from the previous one; no unexplained jumps.',
|
|
295
|
+
'3. Be concrete: name the component, the change, and the observable effect. Vague claims ("improve efficiency", "be better") score 0 on logic.',
|
|
296
|
+
'4. State the strongest counter-risk honestly; an idea with no real risk is not being analyzed.',
|
|
297
|
+
'Output JSON with fields: title (<=60 chars), idea (the full causal chain, 100-250 words), expectedBenefit, risk, novelty (0-100), feasibility (0-100), impact (0-100), logic (0-100: how tight and complete the causal chain is).',
|
|
298
|
+
].join('\n'),
|
|
229
299
|
temperature: strat === 'explore' ? 0.8 : 0.4,
|
|
300
|
+
maxTokens: 900,
|
|
230
301
|
})
|
|
231
302
|
|
|
232
|
-
if (result.data) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
novelty: Math.min(100, Math.max(0, result.data.novelty || 50)),
|
|
240
|
-
feasibility: Math.min(100, Math.max(0, result.data.feasibility || 50)),
|
|
241
|
-
impact: Math.min(100, Math.max(0, result.data.impact || 50)),
|
|
242
|
-
sourceLabels: [a.name, b.name],
|
|
243
|
-
status: 'active',
|
|
244
|
-
createdAt: Date.now(),
|
|
245
|
-
strategy: strat,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Novelty check
|
|
249
|
-
const verdict = evaluateNovelty(h, recent, rejected)
|
|
250
|
-
if (verdict.shouldReject) {
|
|
251
|
-
h.status = 'rejected'
|
|
252
|
-
h.rejectionReason = verdict.rejectReason
|
|
253
|
-
} else {
|
|
254
|
-
h.novelty = verdict.adjustedNovelty
|
|
255
|
-
}
|
|
303
|
+
if (!result || !result.data) {
|
|
304
|
+
errors.push({
|
|
305
|
+
pair: [a.name, b.name],
|
|
306
|
+
error: (result && result.error) || 'LLM returned no data',
|
|
307
|
+
})
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
256
310
|
|
|
257
|
-
|
|
258
|
-
|
|
311
|
+
const h = {
|
|
312
|
+
id: crypto.randomUUID(),
|
|
313
|
+
title: String(result.data.title || 'Untitled').slice(0, 60),
|
|
314
|
+
idea: result.data.idea || '',
|
|
315
|
+
expectedBenefit: result.data.expectedBenefit || '',
|
|
316
|
+
risk: result.data.risk || '',
|
|
317
|
+
novelty: clamp100(result.data.novelty || 50),
|
|
318
|
+
feasibility: clamp100(result.data.feasibility || 50),
|
|
319
|
+
impact: clamp100(result.data.impact || 50),
|
|
320
|
+
// logic is the fourth scoring dimension: a high-novelty idea with a
|
|
321
|
+
// broken causal chain must be visibly weaker than one that reasons
|
|
322
|
+
// cleanly. Defaults to 50 so records written before it existed keep
|
|
323
|
+
// a neutral value instead of NaN.
|
|
324
|
+
logic: clamp100(result.data.logic ?? 50),
|
|
325
|
+
sourceLabels: [a.name, b.name],
|
|
326
|
+
status: 'active',
|
|
327
|
+
createdAt: Date.now(),
|
|
328
|
+
strategy: strat,
|
|
329
|
+
}
|
|
259
330
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
331
|
+
// Novelty check
|
|
332
|
+
const verdict = evaluateNovelty(h, recent, rejected)
|
|
333
|
+
if (verdict.shouldReject) {
|
|
334
|
+
h.status = 'rejected'
|
|
335
|
+
h.rejectionReason = verdict.rejectReason
|
|
336
|
+
} else {
|
|
337
|
+
h.novelty = verdict.adjustedNovelty
|
|
267
338
|
}
|
|
339
|
+
|
|
340
|
+
this.store.addHypothesis(h)
|
|
341
|
+
ideas.push(h)
|
|
342
|
+
|
|
343
|
+
// Record combo
|
|
344
|
+
this.store.addCombo({
|
|
345
|
+
id: crypto.randomUUID(),
|
|
346
|
+
sources: [a.name, b.name],
|
|
347
|
+
description: h.title,
|
|
348
|
+
createdAt: Date.now(),
|
|
349
|
+
})
|
|
268
350
|
} catch (err) {
|
|
269
|
-
|
|
351
|
+
errors.push({
|
|
352
|
+
pair: [a.name, b.name],
|
|
353
|
+
error: String((err && err.message) || err),
|
|
354
|
+
})
|
|
270
355
|
}
|
|
271
356
|
}
|
|
272
357
|
|
|
273
|
-
return { ideas, strategy: strat, pairsAttempted: pairs.length }
|
|
358
|
+
return { ideas, strategy: strat, pairsAttempted: pairs.length, errors }
|
|
274
359
|
}
|
|
275
360
|
|
|
276
361
|
async ferment(limit = 5) {
|
|
@@ -283,48 +368,105 @@ class CreativityEngine {
|
|
|
283
368
|
}
|
|
284
369
|
|
|
285
370
|
const results = []
|
|
371
|
+
// Same reason as generate(): a failing review used to be indistinguishable
|
|
372
|
+
// from a hypothesis that simply had nothing new to say.
|
|
373
|
+
const errors = []
|
|
286
374
|
for (const h of fermentable) {
|
|
287
375
|
const prompt = `Review and refine this hypothesis. Current state:
|
|
288
376
|
Title: ${h.title}
|
|
289
377
|
Idea: ${h.idea}
|
|
290
|
-
Novelty: ${h.novelty}, Feasibility: ${h.feasibility}, Impact: ${h.impact}
|
|
378
|
+
Novelty: ${h.novelty}, Feasibility: ${h.feasibility}, Impact: ${h.impact}, Logic: ${h.logic ?? '(not scored)'}
|
|
291
379
|
|
|
292
|
-
|
|
380
|
+
Review checklist (apply in order):
|
|
381
|
+
1. Causal chain: does the idea explain WHY A leads to B at each step? If a step is asserted without a mechanism, lower logic and rewrite the step.
|
|
382
|
+
2. Evidence: is each claim traceable to the given idea text, or is it speculation presented as fact? Flag speculation.
|
|
383
|
+
3. Falsifiability: can you state how this idea would prove wrong? If not, say so in reason.
|
|
384
|
+
4. Concrete over vague: reject filler claims ("improve efficiency") by rewriting them with the specific component and effect.
|
|
385
|
+
|
|
386
|
+
Verdict rules:
|
|
387
|
+
- promote: the idea is mature AND the refined scores justify landing (novelty + feasibility + impact > 200 with logic >= 60).
|
|
388
|
+
- merge: only when this idea is clearly complementary to another stored one; set mergeWithId to that hypothesis id.
|
|
389
|
+
- reject: the causal chain cannot be repaired, or the premise lost its supporting signals.
|
|
390
|
+
- keep: everything else.
|
|
391
|
+
|
|
392
|
+
Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibility (0-100), impact (0-100), logic (0-100), verdict (promote/keep/reject/merge), reason, mergeWithId (only for merge).`
|
|
293
393
|
|
|
294
394
|
try {
|
|
295
395
|
const result = await this.chatJson(prompt, {
|
|
296
|
-
system: 'You are a creative hypothesis reviewer.
|
|
396
|
+
system: 'You are a critical creative hypothesis reviewer. Reward tight causal reasoning, penalize vagueness. Output JSON only.',
|
|
297
397
|
temperature: 0.3,
|
|
398
|
+
maxTokens: 900,
|
|
298
399
|
})
|
|
299
400
|
|
|
300
|
-
if (result.data) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
401
|
+
if (!result || !result.data) {
|
|
402
|
+
errors.push({
|
|
403
|
+
id: h.id,
|
|
404
|
+
title: h.title,
|
|
405
|
+
error: (result && result.error) || 'LLM returned no data',
|
|
406
|
+
})
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const patch = {
|
|
411
|
+
title: String(result.data.title || h.title).slice(0, 60),
|
|
412
|
+
idea: result.data.idea || h.idea,
|
|
413
|
+
expectedBenefit: result.data.expectedBenefit || h.expectedBenefit,
|
|
414
|
+
risk: result.data.risk || h.risk,
|
|
415
|
+
novelty: clamp100(result.data.novelty || h.novelty),
|
|
416
|
+
feasibility: clamp100(result.data.feasibility || h.feasibility),
|
|
417
|
+
impact: clamp100(result.data.impact || h.impact),
|
|
418
|
+
logic: clamp100(result.data.logic ?? h.logic ?? 50),
|
|
419
|
+
fermentedAt: Date.now(),
|
|
420
|
+
fermentCount: (h.fermentCount || 0) + 1,
|
|
421
|
+
}
|
|
312
422
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
423
|
+
let verdict = result.data.verdict
|
|
424
|
+
if (verdict === 'reject') {
|
|
425
|
+
patch.status = 'rejected'
|
|
426
|
+
} else if (verdict === 'merge') {
|
|
427
|
+
// merge used to fall through silently: the verdict was echoed back
|
|
428
|
+
// in the results but nothing was merged. Now it behaves like
|
|
429
|
+
// IdeaFermentationEngine -- the reviewed idea is folded into the
|
|
430
|
+
// target and marked merged, or downgraded to keep when the target
|
|
431
|
+
// id is unknown (an unresolvable merge is not a merge).
|
|
432
|
+
const target = result.data.mergeWithId
|
|
433
|
+
? this.store.getHypotheses().find((x) => x.id === result.data.mergeWithId)
|
|
434
|
+
: null
|
|
435
|
+
if (target && target.id !== h.id) {
|
|
436
|
+
target.idea = [target.idea, patch.idea].filter(Boolean).join('\n---\n')
|
|
437
|
+
target.fermentedAt = patch.fermentedAt
|
|
438
|
+
target.fermentCount = (target.fermentCount || 0) + 1
|
|
439
|
+
this.store.updateHypothesis(target.id, {
|
|
440
|
+
idea: target.idea,
|
|
441
|
+
fermentedAt: target.fermentedAt,
|
|
442
|
+
fermentCount: target.fermentCount,
|
|
443
|
+
})
|
|
444
|
+
patch.status = 'merged'
|
|
445
|
+
patch.mergedInto = target.id
|
|
446
|
+
} else {
|
|
447
|
+
// An unresolvable merge target is not a merge; report it honestly
|
|
448
|
+
// as keep so the caller never sees a merge that did not happen.
|
|
449
|
+
verdict = 'keep'
|
|
317
450
|
}
|
|
451
|
+
}
|
|
318
452
|
|
|
319
|
-
|
|
320
|
-
|
|
453
|
+
// The gate is unconditional: patch.logic already folds in the
|
|
454
|
+
// review's score, the stored score, or 50. A reviewer that omits
|
|
455
|
+
// the field falls back to the stored/neutral value — omitting logic
|
|
456
|
+
// must not waive the gate it exists to enforce.
|
|
457
|
+
const logicOk = Number.isFinite(patch.logic) && patch.logic >= 60
|
|
458
|
+
if (verdict === 'promote' && (patch.novelty + patch.feasibility + patch.impact) > 200 && logicOk) {
|
|
459
|
+
patch.status = 'validated'
|
|
321
460
|
}
|
|
461
|
+
|
|
462
|
+
this.store.updateHypothesis(h.id, patch)
|
|
463
|
+
results.push({ id: h.id, title: h.title, verdict, reason: result.data.reason })
|
|
322
464
|
} catch (err) {
|
|
323
|
-
|
|
465
|
+
errors.push({ id: h.id, title: h.title, error: String((err && err.message) || err) })
|
|
324
466
|
}
|
|
325
467
|
}
|
|
326
468
|
|
|
327
|
-
return { fermented: results.length, results }
|
|
469
|
+
return { fermented: results.length, results, errors }
|
|
328
470
|
}
|
|
329
471
|
|
|
330
472
|
status() {
|
|
@@ -332,44 +474,58 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impa
|
|
|
332
474
|
const active = this.store.getHypotheses({ status: 'active' })
|
|
333
475
|
const validated = this.store.getHypotheses({ status: 'validated' })
|
|
334
476
|
const rejected = this.store.getHypotheses({ status: 'rejected' })
|
|
477
|
+
// draft is a real state -- ferment() reviews draft + active -- so hiding it
|
|
478
|
+
// here made `status` and `list --status draft` disagree.
|
|
479
|
+
const draft = this.store.getHypotheses({ status: 'draft' })
|
|
335
480
|
|
|
336
481
|
return {
|
|
337
482
|
...count,
|
|
338
483
|
active: active.length,
|
|
339
484
|
validated: validated.length,
|
|
340
485
|
rejected: rejected.length,
|
|
486
|
+
draft: draft.length,
|
|
341
487
|
recentIdeas: active.slice(-5).map(h => ({
|
|
342
488
|
id: h.id,
|
|
343
489
|
title: h.title,
|
|
344
490
|
novelty: h.novelty,
|
|
345
491
|
feasibility: h.feasibility,
|
|
346
492
|
impact: h.impact,
|
|
493
|
+
// Undefined keys vanish over JSON (the CLI path); keep them out here
|
|
494
|
+
// too so the engine and the CLI round-trip deep-equal.
|
|
495
|
+
...(h.logic !== undefined ? { logic: h.logic } : {}),
|
|
347
496
|
score: h.novelty + h.feasibility + h.impact,
|
|
348
497
|
})),
|
|
349
498
|
}
|
|
350
499
|
}
|
|
351
500
|
|
|
501
|
+
// The MCP schema and the CLI usage text both promise a default of 20; the
|
|
502
|
+
// implementation returned the whole store, so a large one flooded the tool
|
|
503
|
+
// result. `limit: 0` (and only 0) opts back into "everything".
|
|
352
504
|
list(opts = {}) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
status:
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
createdAt
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}))
|
|
505
|
+
const limit = opts.limit === undefined ? 20 : opts.limit
|
|
506
|
+
if (opts.sort === 'novelty') {
|
|
507
|
+
// Relative novelty ranking (read-only, nothing written back). Pairwise
|
|
508
|
+
// Jaccard is O(n²) — bound candidates to the newest 200 (spec Risk 3).
|
|
509
|
+
const candidates = this.store.getHypotheses({ status: opts.status }).slice(-200)
|
|
510
|
+
const rows = candidates.map((h) => {
|
|
511
|
+
const others = candidates.filter((x) => x.id !== h.id)
|
|
512
|
+
const verdict = evaluateNovelty(h, others, [])
|
|
513
|
+
return { ...listRow(h), noveltyScore: verdict.adjustedNovelty }
|
|
514
|
+
})
|
|
515
|
+
rows.sort((a, b) => b.noveltyScore - a.noveltyScore || (b.createdAt || 0) - (a.createdAt || 0))
|
|
516
|
+
return limit === 0 ? rows : rows.slice(0, limit)
|
|
517
|
+
}
|
|
518
|
+
return this.store.getHypotheses({ status: opts.status, limit }).map(listRow)
|
|
367
519
|
}
|
|
368
520
|
|
|
369
521
|
_buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
|
|
522
|
+
// Long sources used to be pasted unbounded, crowding the instructions out
|
|
523
|
+
// of the context window; 600 chars keeps the pairing signal without the
|
|
524
|
+
// flood.
|
|
525
|
+
const clip = (s, n = 600) => (s.length > n ? s.slice(0, n) + '…' : s)
|
|
370
526
|
let prompt = `Combine these two concepts into ONE novel hypothesis:\n\n`
|
|
371
|
-
prompt += `Concept A: "${a.name}" — ${a.content}\n`
|
|
372
|
-
prompt += `Concept B: "${b.name}" — ${b.content}\n\n`
|
|
527
|
+
prompt += `Concept A: "${a.name}" — ${clip(a.content)}\n`
|
|
528
|
+
prompt += `Concept B: "${b.name}" — ${clip(b.content)}\n\n`
|
|
373
529
|
prompt += `Strategy: ${strategy}\n`
|
|
374
530
|
|
|
375
531
|
if (strategy === 'explore') {
|
|
@@ -380,6 +536,15 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impa
|
|
|
380
536
|
prompt += `Stable combination. Conservative refinement of existing ideas.\n`
|
|
381
537
|
}
|
|
382
538
|
|
|
539
|
+
// The logical-quality bar, stated where the model composes rather than
|
|
540
|
+
// only in the system prompt: synergy mechanism, explicit causal chain,
|
|
541
|
+
// and no claims that cannot name their mechanism.
|
|
542
|
+
prompt += `\nQuality requirements:\n`
|
|
543
|
+
prompt += `- The idea must explain the synergy mechanism: WHY these two concepts combine.\n`
|
|
544
|
+
prompt += `- Write the proposal as a causal chain (premise -> mechanism -> outcome), each step following from the last.\n`
|
|
545
|
+
prompt += `- Be specific: component, change, observable effect. No bare claims like "improve efficiency".\n`
|
|
546
|
+
prompt += `- The title must be self-contained (a reader sees only the title in lists).\n`
|
|
547
|
+
|
|
383
548
|
if (recentHypotheses.length > 0) {
|
|
384
549
|
prompt += `\nRecent ideas (avoid duplicates):\n`
|
|
385
550
|
for (const h of recentHypotheses) {
|
|
@@ -394,9 +559,9 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impa
|
|
|
394
559
|
}
|
|
395
560
|
}
|
|
396
561
|
|
|
397
|
-
prompt += `\nOutput JSON: {"title": "...", "idea": "...", "expectedBenefit": "...", "risk": "...", "novelty": 0-100, "feasibility": 0-100, "impact": 0-100}`
|
|
562
|
+
prompt += `\nOutput JSON: {"title": "...", "idea": "...", "expectedBenefit": "...", "risk": "...", "novelty": 0-100, "feasibility": 0-100, "impact": 0-100, "logic": 0-100}`
|
|
398
563
|
return prompt
|
|
399
564
|
}
|
|
400
565
|
}
|
|
401
566
|
|
|
402
|
-
module.exports = { CreativityEngine, CreativityStore }
|
|
567
|
+
module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights, isoOf }
|