mio-agent-runtime 0.13.5 → 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 CHANGED
@@ -57,6 +57,7 @@ mio creativity status Show creativity hypothesis counts and recent top id
57
57
  mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --limit N)
58
58
  mio creativity generate Generate hypotheses from 2+ --source "name|content" (or --from-insights; calls an LLM)
59
59
  mio creativity ferment Review and refine active hypotheses (calls an LLM)
60
+ mio idea generate --goal "..." Grounded idea pipeline: auto sources + novelty gate, persisted (calls an LLM)
60
61
  mio insight status Insight counts: total, reported, unreported, high-value (needs @akemi-mio/insight)
61
62
  mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
62
63
  mio insight generate Generate insights from context (--memory "kind|content"/--summary "text"; calls an LLM)
@@ -101,7 +102,7 @@ mio --version Print the version and exit (-V)
101
102
 
102
103
  ## MCP 工具
103
104
 
104
- MCP 服务端在 5 大域共暴露 50 个工具:
105
+ MCP 服务端在 5 大域共暴露 51 个工具:
105
106
 
106
107
  ### 记忆(11)
107
108
  `mio.memory.query` · `mio.memory.record` · `mio.memory.archive` · `mio.memory.merge` · `mio.memory.migrate` · `mio.memory.analyze` · `mio.memory.forget` · `mio.experience.list` · `mio.experience.confirm` · `mio.experience.reuse` · `mio.policy.check`
@@ -114,15 +115,15 @@ MCP 服务端在 5 大域共暴露 50 个工具:
114
115
  ### 洞察自省(4)
115
116
  `mio.insight.status` · `mio.insight.list` · `mio.insight.mark_reported` · `mio.insight.generate`
116
117
 
117
- ### 创意引擎(4)
118
- `mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status`
118
+ ### 创意引擎(5)
119
+ `mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status` · `mio.idea.generate`
119
120
 
120
121
  ### 任务、Agent 与演化(16)
121
122
  `mio.task.route` · `mio.task.record_outcome` · `mio.agent.register` · `mio.agent.list` · `mio.agent.report` · `mio.agent.evaluation` · `mio.host.capabilities` · `mio.evolution.status` · `mio.evolution.report` · `mio.evolution.shadow.record` · `mio.evolution.dual_write.record` · `mio.evolution.cutover.readiness` · `mio.evolution.cutover.apply` · `mio.evolution.migration.plan` · `mio.evolution.authority.plan` · `mio.phase0.report`
122
123
 
123
124
  ## 包
124
125
 
125
- `mio-agent-runtime` 依赖发布在 `@akemi-mio` scope 下的 **9** 个包(声明于 `package.json`,均为 `0.1.0`)。它们仅在源码变更时才发布,所以普通的 CLI 发布不会重新发布这些包。
126
+ `mio-agent-runtime` 依赖发布在 `@akemi-mio` scope 下的 **10** 个包(声明于 `package.json`,均为 `0.1.0`)。它们仅在源码变更时才发布,所以普通的 CLI 发布不会重新发布这些包。
126
127
 
127
128
  | 依赖 | 说明 |
128
129
  |---|---|
@@ -135,6 +136,7 @@ MCP 服务端在 5 大域共暴露 50 个工具:
135
136
  | `@akemi-mio/evolution-scheduler` | 演化调度器 |
136
137
  | `@akemi-mio/observer` | ObserverService —— 多源数据采集、趋势分析、深度研究、世界模型、发酵、DAG 状态机、自演化引擎(唯一运行时依赖 `undici`,只用于走代理采集) |
137
138
  | `@akemi-mio/insight` | InsightService —— 基于 LLM 的洞察生成、冲突/漂移/重复/停滞目标/摩擦检测器、在场服务、洞察打分(零 npm 依赖) |
139
+ | `@akemi-mio/creativity` | IdeaGenerator / NoveltyScorer / SourceAggregator —— `mio idea generate` 与创意引擎的生成算法(依赖 `@akemi-mio/core`) |
138
140
 
139
141
  以下 `@akemi-mio` 包同样位于本 monorepo 中并独立发布,但**不是**本 CLI 的依赖——其源码从不 import 它们,因此未声明在 `package.json` 里:
140
142
 
@@ -146,9 +148,9 @@ MCP 服务端在 5 大域共暴露 50 个工具:
146
148
 
147
149
  它们可直接被其它 Node 项目消费。`mio-agent-runtime` 自身是 CLI + MCP server 包。
148
150
 
149
- ### 与宿主耦合的包(不发布)
151
+ ### 曾经不发布的两个包(现已发布)
150
152
 
151
- `@akemi-mio/core` 与 `@akemi-mio/creativity` 通过 `workspace:*` 链接在本 monorepo 内部消费,因此从源码构建,**不**发布到 npm。`packages/cli/` 已不存在。不要把这两个包加进下游 `package.json`——安装会失败。
153
+ `@akemi-mio/core` 与 `@akemi-mio/creativity` 自 `mio-agent-runtime@0.14.0` 起发布到 npm(`0.1.0`)。`@akemi-mio/creativity` 是本 CLI 的直接依赖,`@akemi-mio/core` 经由它传递引入;其它 Node 项目可直接安装这两个包。
152
154
 
153
155
  ## 宿主说明
154
156
 
@@ -414,7 +416,7 @@ mio creativity ferment --limit 3
414
416
 
415
417
  - `--source "名称|内容"` **至少两个**:引擎是把概念两两配对来产生新假设的,
416
418
  一个来源在构造上就不可能产出组合(此时**不会**调用 LLM,直接返回
417
- `need at least 2 sources`)。
419
+ `need at least 2 sources`)。显式来源不足 2 个时,本地自动来源(memory / traces / 已存假设;MCP 侧还含 insight 与 observer 趋势)会先把列表补齐到 2——显式 `--source` 永远排在前面且不被替换;补齐后仍不足 2 个才报上面的错。
418
420
  - **`--from-insights`(MCP 侧为 `fromInsights: true`)用已存洞察补足概念来源**:
419
421
  读取 `mio insight list` 能看到的同一份存储,按分数取前 10 条映射成
420
422
  `{ name, content, type: 'insight' }`,再与显式的 `--source` 合并。它只在
@@ -428,10 +430,24 @@ mio creativity ferment --limit 3
428
430
  打印 `N pair(s) failed:`。此前 `catch {}` 让「端点挂了」与「这一对确实没有
429
431
  新意」长得一模一样——`ideas: []` / `Nothing fermented` 两种输出都无法区分。
430
432
 
433
+ ### 目标驱动的 idea 流水线(`mio idea generate`)
434
+
435
+ 一个 goal 直接产出可实验的假设(MCP 面为 `mio.idea.generate`),来源全程本地优先组装:
436
+
437
+ ```bash
438
+ mio idea generate --goal "降低 MCP 调用延迟" --context "近三天 p95 上升" --constraint "不引入新依赖"
439
+ mio idea generate --goal "扩大命令覆盖" --json
440
+ ```
441
+
442
+ - **来源顺序**:goal → context → constraints → 记忆 grounding(取前 3 条,`scope: all`)→ 本地自动来源(memory / traces / 已存假设,MCP 侧还含 insight 与 observer 趋势),按 `origin` 去重;凑不足 2 个时返回 `reason` 文案而不是报错。
443
+ - **每条 idea 都持久化**到 `<MIO_HOME>/creativity/creativity-hypotheses.jsonl`(`draft`,带 `provenance`:strategy / technique / relatedMemoryIds / 命中来源 / generatedAt),**绝不写入 Mio memory**。
444
+ - **novelty 门禁**:与已 `rejected` 假设近似重复的候选落盘为 `rejected` 并带 `rejectionReason`;与近期假设相似的只降分。拒绝的假设同样入库,供后续去重。
445
+ - `--num` 钳制在 1..3(默认 3);`--json` 返回完整结构 `{ ideas, generatedAt, groundedWith, persistedIds }`。
446
+
431
447
  #### LLM 配置
432
448
 
433
- 只有 4 条命令会调用大模型:`mio creativity generate` / `ferment`、
434
- `mio insight generate`、`mio observer ferment`。它们共用
449
+ 只有 5 条命令会调用大模型:`mio creativity generate` / `ferment`、
450
+ `mio idea generate`、`mio insight generate`、`mio observer ferment`。它们共用
435
451
  `server/llm-client.js` 这一份实现,配置也共用同一套解析顺序:
436
452
 
437
453
  ```
package/bin/mio.js CHANGED
@@ -24,6 +24,8 @@ const { createMemoryStore } = require('../server/memory-store.js')
24
24
  const { createExperienceStore } = require('../server/experience-store.js')
25
25
  const { createPolicyStore } = require('../server/policy-store.js')
26
26
  const { CreativityEngine, sourcesFromInsights } = require('../server/creativity-engine.js')
27
+ const { buildAutoSources, topUpSources } = require('../server/creativity-sources.js')
28
+ const { runIdeaGenerate, groundingFrom } = require('../server/idea-generate.js')
27
29
  const { createAgentStore } = require('../server/agent-store.js')
28
30
  const { createEvaluationStore } = require('../server/evaluation-store.js')
29
31
  const { createInsightStore, isInsightAvailable } = require('../server/insight-store.js')
@@ -2065,7 +2067,7 @@ function observerCommand(args, useJson) {
2065
2067
  function creativityUsage(write = console.log) {
2066
2068
  write(`Usage:
2067
2069
  mio creativity status Show hypothesis counts and recent top ideas
2068
- mio creativity list List hypotheses (--status active|validated|rejected|draft, --limit N)
2070
+ mio creativity list List hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
2069
2071
 
2070
2072
  mio creativity generate Generate hypotheses by combining sources (needs 2+ --source, calls an LLM)
2071
2073
  mio creativity ferment Review/refine active hypotheses (calls an LLM)
@@ -2073,6 +2075,7 @@ function creativityUsage(write = console.log) {
2073
2075
  Options:
2074
2076
  --status name Filter list by status
2075
2077
  --limit N Max results for list / ferment (default 20 / 5); --limit 0 lists everything
2078
+ --sort novelty Rank list by relative noveltyScore (a dedup signal, not a quality score)
2076
2079
  --source "name|content" A concept source for generate; repeat 2+ times
2077
2080
  --from-insights Seed generate's sources from stored insights (needs @akemi-mio/insight)
2078
2081
  --strategy explore|signal|stable Generation strategy (auto-selected if omitted)
@@ -2081,6 +2084,7 @@ Options:
2081
2084
  Examples:
2082
2085
  mio creativity status
2083
2086
  mio creativity list --status rejected --limit 10
2087
+ mio creativity list --sort novelty
2084
2088
  mio creativity generate --source "auth|token rotation" --source "cache|write-through"
2085
2089
  mio creativity generate --from-insights --source "auth|token rotation"
2086
2090
 
@@ -2114,7 +2118,7 @@ function printCreativityList(items) {
2114
2118
  items.forEach((h, index) => {
2115
2119
  const when = h.createdAt ? new Date(h.createdAt).toISOString().slice(0, 19).replace('T', ' ') : ''
2116
2120
  const labels = Array.isArray(h.sourceLabels) && h.sourceLabels.length > 0 ? h.sourceLabels.join(' + ') : '—'
2117
- console.log(`${index + 1}. [${h.status}] ${h.title} (N=${h.novelty} F=${h.feasibility} I=${h.impact} score=${h.score})`)
2121
+ console.log(`${index + 1}. [${h.status}] ${h.title} (N=${h.novelty} F=${h.feasibility} I=${h.impact} score=${h.score}${h.noveltyScore !== undefined ? ` noveltyScore=${h.noveltyScore}` : ''})`)
2118
2122
  console.log(` ${labels} | ${when} | ${h.id}`)
2119
2123
  if (h.fermentCount) console.log(` fermented ${h.fermentCount}x`)
2120
2124
  })
@@ -2162,6 +2166,26 @@ function printCreativityErrors(errors, label) {
2162
2166
  }
2163
2167
  }
2164
2168
 
2169
+ // Auto sources for generate's top-up: the same local providers the MCP server
2170
+ // uses, assembled from the CLI's global MIO_HOME (spec Capability 2). Every
2171
+ // layer degrades to [] — a missing optional package or an unreadable store
2172
+ // must never fail `mio creativity generate`.
2173
+ function cliAutoSources() {
2174
+ let insights = []
2175
+ let trends = []
2176
+ try {
2177
+ if (isInsightAvailable()) insights = cliInsightStore().list({}) || []
2178
+ } catch (_) {}
2179
+ try {
2180
+ if (isObserverAvailable()) trends = cliObserverStore().trends({ limit: 3 }) || []
2181
+ } catch (_) {}
2182
+ try {
2183
+ return buildAutoSources({ dataDir: MIO_HOME, insights, trends })
2184
+ } catch (_) {
2185
+ return []
2186
+ }
2187
+ }
2188
+
2165
2189
  function creativityGenerateCommand(args, useJson) {
2166
2190
  const flags = args.slice(2)
2167
2191
  let sources = parseSources(flags)
@@ -2191,6 +2215,15 @@ function creativityGenerateCommand(args, useJson) {
2191
2215
  sources = sources.concat(seeds)
2192
2216
  }
2193
2217
 
2218
+ // Auto top-up (spec Capability 2, injection point 2): explicit --source
2219
+ // entries win; when fewer than two remain, local memory/traces/stored
2220
+ // hypotheses (plus insight/observer observations) fill the gap. If the
2221
+ // topped-up set is still short, the errors below keep their old wording.
2222
+ if (sources.length < 2) {
2223
+ const topped = topUpSources(sources, cliAutoSources())
2224
+ if (topped.length >= 2) sources = topped
2225
+ }
2226
+
2194
2227
  if (sources.length < 2) {
2195
2228
  if (flagPresent(flags, '--from-insights')) {
2196
2229
  console.error(`mio creativity generate needs at least 2 concept sources; --from-insights only supplied ${sources.length}.`)
@@ -2263,6 +2296,90 @@ function creativityFermentCommand(args, useJson) {
2263
2296
  })
2264
2297
  }
2265
2298
 
2299
+ function ideaUsage() {
2300
+ console.error('usage: mio idea generate --goal "one line" [--context "..."] [--constraint "..."] [--num N] [--json]')
2301
+ }
2302
+
2303
+ async function ideaGenerateCommand(args, useJson) {
2304
+ const flags = args.slice(2)
2305
+ const goal = (optionValue(flags, '--goal') || '').trim()
2306
+ if (!goal) {
2307
+ console.error('mio idea generate requires --goal "..."')
2308
+ ideaUsage()
2309
+ process.exitCode = 1
2310
+ return
2311
+ }
2312
+ const context = optionValue(flags, '--context') || ''
2313
+ const constraints = optionValues(flags, '--constraint')
2314
+ let numIdeas
2315
+ try {
2316
+ numIdeas = parseNumberOption(flags, '--num')
2317
+ } catch (error) {
2318
+ console.error(error.message || error)
2319
+ ideaUsage()
2320
+ process.exitCode = 1
2321
+ return
2322
+ }
2323
+
2324
+ // Grounding from the CLI's global MIO_HOME. A failed query degrades to "no
2325
+ // grounding" — never a hard error (spec Capability 1: local-first, tolerant).
2326
+ // The core logger echoes every event to stdout; in --json mode that would
2327
+ // corrupt the payload jsonOrText must own, so the echo is routed to stderr
2328
+ // while the pipeline runs (executed deviation — the plan assumed pure JSON).
2329
+ const echoLog = useJson ? console.log : null
2330
+ if (useJson) console.log = (...params) => console.error(...params)
2331
+
2332
+ let result
2333
+ try {
2334
+ let grounding = { sources: [], relatedMemoryIds: [], groundedWith: 0 }
2335
+ try {
2336
+ const qr = await cliMemoryStore().queryMemory({ query: goal, limit: 3, scope: 'all' })
2337
+ grounding = groundingFrom(qr)
2338
+ } catch (_) {}
2339
+
2340
+ if (!useJson) warnIfLlmUnconfigured()
2341
+
2342
+ result = await runIdeaGenerate({
2343
+ goal,
2344
+ context,
2345
+ constraints,
2346
+ numIdeas,
2347
+ chatJson,
2348
+ store: cliCreativityEngine().store,
2349
+ groundingSources: grounding.sources,
2350
+ relatedMemoryIds: grounding.relatedMemoryIds,
2351
+ autoSources: cliAutoSources(),
2352
+ })
2353
+ } catch (error) {
2354
+ console.error(error.message || error)
2355
+ process.exitCode = 1
2356
+ return
2357
+ } finally {
2358
+ if (useJson) console.log = echoLog
2359
+ }
2360
+
2361
+ if (useJson) return jsonOrText(result, true)
2362
+ if (result.reason) {
2363
+ console.log(`No ideas generated: ${result.reason}`)
2364
+ return
2365
+ }
2366
+ console.log(`Generated ${result.ideas.length} idea(s) (grounded with ${result.groundedWith} memories)`)
2367
+ result.ideas.forEach((idea) => {
2368
+ console.log(`- [${idea.hypothesis.status}] ${idea.title}`)
2369
+ console.log(` novelty=${idea.hypothesis.novelty} feasibility=${idea.hypothesis.feasibility} impact=${idea.hypothesis.impact} | ${idea.hypothesis.id}`)
2370
+ if (idea.provenance && idea.provenance.technique) console.log(` technique=${idea.provenance.technique}`)
2371
+ if (idea.hypothesis.rejectionReason) console.log(` rejected: ${idea.hypothesis.rejectionReason}`)
2372
+ })
2373
+ console.log(`Persisted ${result.persistedIds.length} idea(s)`)
2374
+ }
2375
+
2376
+ function ideaCommand(args, useJson) {
2377
+ const sub = args[1]
2378
+ if (sub === 'generate') return ideaGenerateCommand(args, useJson)
2379
+ ideaUsage()
2380
+ process.exitCode = 1
2381
+ }
2382
+
2266
2383
  function creativityCommand(args, useJson) {
2267
2384
  const sub = args[1]
2268
2385
 
@@ -2289,9 +2406,16 @@ function creativityCommand(args, useJson) {
2289
2406
  if (sub === 'status') {
2290
2407
  result = engine.status()
2291
2408
  } else {
2409
+ const sort = optionValue(flags, '--sort')
2410
+ if (sort !== undefined && sort !== 'novelty') {
2411
+ console.error(`mio creativity list: unknown --sort "${sort}" (supported: novelty)`)
2412
+ process.exitCode = 1
2413
+ return
2414
+ }
2292
2415
  result = engine.list({
2293
2416
  status: optionValue(flags, '--status'),
2294
2417
  limit: parseNumberOption(flags, '--limit'),
2418
+ sort,
2295
2419
  })
2296
2420
  }
2297
2421
  } catch (error) {
@@ -3369,9 +3493,10 @@ Usage:
3369
3493
  mio experience confirm --ids a,b Confirm auto-claimed reuse (bulk supported)
3370
3494
  mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse
3371
3495
  mio creativity status Show creativity hypothesis counts and recent top ideas
3372
- mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --limit N)
3496
+ mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
3373
3497
  mio creativity generate Generate hypotheses from 2+ --source "name|content" (or --from-insights; calls an LLM)
3374
3498
  mio creativity ferment Review and refine active hypotheses (calls an LLM)
3499
+ mio idea generate --goal "..." Grounded idea pipeline: auto sources, novelty gate, persisted (calls an LLM)
3375
3500
  mio insight status Insight counts: total, reported, unreported, high-value
3376
3501
  mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
3377
3502
  mio insight generate Generate insights from context (--memory "kind|content"/--summary "text"; calls an LLM)
@@ -3435,6 +3560,8 @@ async function main() {
3435
3560
  return policyCommand(args, useJson)
3436
3561
  case 'creativity':
3437
3562
  return creativityCommand(args, useJson)
3563
+ case 'idea':
3564
+ return ideaCommand(args, useJson)
3438
3565
  case 'insight':
3439
3566
  return insightCommand(args, useJson)
3440
3567
  case 'observer':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.13.5",
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",
@@ -34,7 +35,8 @@
34
35
  "@akemi-mio/evolution-safety": "^0.1.0",
35
36
  "@akemi-mio/evolution-scheduler": "^0.1.0",
36
37
  "@akemi-mio/observer": "^0.1.4",
37
- "@akemi-mio/insight": "^0.1.1"
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,7 @@
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')
6
7
 
7
8
  // Cache validity key for a JSONL file: a (size, mtimeMs) pair, the same
8
9
  // guard server/insight-store.js uses (commit 0ba7fa8). Every getter below used
@@ -27,6 +28,13 @@ function clamp100(value, fallback = 50) {
27
28
  return Math.min(100, Math.max(0, n))
28
29
  }
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
+ }
37
+
30
38
  // ═══════════════════════════════════════════════
31
39
  // JSONL Store — ideas, combos, experiments
32
40
  // ═══════════════════════════════════════════════
@@ -173,43 +181,6 @@ function mixConcepts(sources, exploredPairs, rng) {
173
181
  return pairs.slice(0, Math.min(5, pairs.length))
174
182
  }
175
183
 
176
- // ═══════════════════════════════════════════════
177
- // Novelty Scorer — reject duplicates
178
- // ═══════════════════════════════════════════════
179
-
180
- function bigramSet(text) {
181
- const s = new Set()
182
- const lower = text.toLowerCase()
183
- for (let i = 0; i < lower.length - 1; i++) {
184
- s.add(lower.slice(i, i + 2))
185
- }
186
- return s
187
- }
188
-
189
- function jaccard(a, b) {
190
- if (a.size === 0 && b.size === 0) return 1
191
- let inter = 0
192
- for (const x of a) if (b.has(x)) inter++
193
- return inter / (a.size + b.size - inter)
194
- }
195
-
196
- function evaluateNovelty(idea, recent, rejected) {
197
- const ideaBigrams = bigramSet(idea.title + ' ' + idea.idea)
198
- for (const r of recent) {
199
- const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
200
- if (sim > 0.7) {
201
- return { shouldReject: true, rejectReason: `too similar to recent: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
202
- }
203
- }
204
- for (const r of rejected) {
205
- const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
206
- if (sim > 0.6) {
207
- return { shouldReject: true, rejectReason: `matches rejected: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
208
- }
209
- }
210
- return { shouldReject: false, adjustedNovelty: Math.min(100, idea.novelty + 5) }
211
- }
212
-
213
184
  // ═══════════════════════════════════════════════
214
185
  // Creativity Engine — main orchestrator
215
186
  // ═══════════════════════════════════════════════
@@ -234,12 +205,38 @@ function sourcesFromInsights(insights, limit = 10) {
234
205
  Array.isArray(i.evidence) && i.evidence.length > 0 ? `Evidence: ${i.evidence.join('; ')}` : '',
235
206
  ].filter(Boolean).join('\n'),
236
207
  type: 'insight',
237
- weight: 0.7,
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) } : {}),
238
213
  }))
239
214
  }
240
215
 
241
216
  const STRATEGY_CYCLE = ['explore', 'signal', 'stable']
242
217
 
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 } : {}),
237
+ }
238
+ }
239
+
243
240
  class CreativityEngine {
244
241
  constructor(dataDir, chatJson) {
245
242
  this.store = new CreativityStore(dataDir)
@@ -506,22 +503,19 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
506
503
  // result. `limit: 0` (and only 0) opts back into "everything".
507
504
  list(opts = {}) {
508
505
  const limit = opts.limit === undefined ? 20 : opts.limit
509
- return this.store.getHypotheses({ ...opts, limit }).map(h => ({
510
- id: h.id,
511
- title: h.title,
512
- idea: h.idea,
513
- status: h.status,
514
- novelty: h.novelty,
515
- feasibility: h.feasibility,
516
- impact: h.impact,
517
- ...(h.logic !== undefined ? { logic: h.logic } : {}),
518
- score: h.novelty + h.feasibility + h.impact,
519
- sourceLabels: h.sourceLabels,
520
- createdAt: h.createdAt,
521
- fermentedAt: h.fermentedAt,
522
- fermentCount: h.fermentCount,
523
- ...(h.mergedInto !== undefined ? { mergedInto: h.mergedInto } : {}),
524
- }))
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)
525
519
  }
526
520
 
527
521
  _buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
@@ -570,4 +564,4 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
570
564
  }
571
565
  }
572
566
 
573
- module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights }
567
+ module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights, isoOf }
@@ -0,0 +1,186 @@
1
+ 'use strict'
2
+
3
+ // Source auto-aggregation for the creativity engine (spec Capability 2).
4
+ //
5
+ // Default providers read ONLY local MIO_HOME data — zero network. Insight and
6
+ // observer sources arrive as plain arrays from the caller, because only the
7
+ // entry point knows which store is authoritative (MIO_HOME for the CLI, the
8
+ // MCP server's MIO_DATA_DIR) and whether its optional package resolves.
9
+ // SourceAggregator already tolerates a throwing provider (per-provider
10
+ // try/catch with a WARN log), so providers here do not need extra guards.
11
+
12
+ const fs = require('fs')
13
+ const path = require('path')
14
+ const { SourceAggregator } = require('@akemi-mio/creativity/SourceAggregator')
15
+ const { sourcesFromInsights, isoOf } = require('./creativity-engine.js')
16
+
17
+ function readJsonl(file) {
18
+ try {
19
+ return fs
20
+ .readFileSync(file, 'utf8')
21
+ .split('\n')
22
+ .filter(Boolean)
23
+ .map((line) => {
24
+ try { return JSON.parse(line) } catch { return null }
25
+ })
26
+ .filter(Boolean)
27
+ } catch (_) {
28
+ return []
29
+ }
30
+ }
31
+
32
+ // Every weight is > 0.7 on purpose: IdeaGenerator.applyTemperature keeps any
33
+ // source with weight > 0.7 unconditionally (temperature 0.5 middle branch), so
34
+ // the assembled set reaches the prompt intact — deterministic, no RNG survival.
35
+ const WEIGHTS = {
36
+ memory: 0.75,
37
+ traceOk: 0.74,
38
+ traceFail: 0.73,
39
+ hypothesisActive: 0.72,
40
+ hypothesisRejected: 0.72,
41
+ trend: 0.76,
42
+ }
43
+
44
+ function memoryProvider(dataDir) {
45
+ return {
46
+ domain: 'feedback',
47
+ name: 'memory',
48
+ collect() {
49
+ return readJsonl(path.join(dataDir, 'memory.jsonl'))
50
+ .filter((r) => r && r.archived !== true && (r.kind === 'decision' || r.kind === 'note'))
51
+ .slice(-6)
52
+ .map((r) => ({
53
+ name: `${r.kind}: ${String(r.content || '').slice(0, 40)}`,
54
+ content: String(r.content || ''),
55
+ type: 'feedback',
56
+ weight: WEIGHTS.memory,
57
+ origin: `memory:${r.id}`,
58
+ originId: r.id,
59
+ timestamp: isoOf(r.timestamp),
60
+ }))
61
+ },
62
+ }
63
+ }
64
+
65
+ function traceProviders(dataDir) {
66
+ const events = () => readJsonl(path.join(dataDir, 'traces.jsonl')).slice(-40)
67
+ const fmt = (r) => `${r.event_type}${r.outcome ? `/${r.outcome}` : ''}: ${JSON.stringify(r.payload || {}).slice(0, 300)}`
68
+ const stamp = (r) => isoOf(r.timestamp ?? r.ts)
69
+ return [
70
+ {
71
+ domain: 'behavior',
72
+ name: 'traces-success',
73
+ collect() {
74
+ return events()
75
+ .filter((r) => r.event_type === 'task_outcome' && r.outcome === 'success')
76
+ .slice(-3)
77
+ .map((r) => ({
78
+ name: `success: ${String((r.payload && (r.payload.task || r.payload.summary)) || r.trace_id || 'task').slice(0, 40)}`,
79
+ content: fmt(r),
80
+ type: 'behavior',
81
+ weight: WEIGHTS.traceOk,
82
+ origin: `trace:${r.trace_id}`,
83
+ originId: r.trace_id,
84
+ timestamp: stamp(r),
85
+ }))
86
+ },
87
+ },
88
+ {
89
+ domain: 'failure',
90
+ name: 'traces-failure',
91
+ collect() {
92
+ return events()
93
+ .filter(
94
+ (r) =>
95
+ (r.event_type === 'error' || r.event_type === 'retry') ||
96
+ (r.event_type === 'task_outcome' && (r.outcome === 'failure' || r.outcome === 'error' || r.outcome === 'aborted')),
97
+ )
98
+ .slice(-3)
99
+ .map((r) => ({
100
+ name: `failure: ${String(r.event_type)}${r.trace_id ? ` ${String(r.trace_id).slice(0, 12)}` : ''}`,
101
+ content: fmt(r),
102
+ type: 'failure',
103
+ weight: WEIGHTS.traceFail,
104
+ origin: `trace:${r.trace_id || 'unknown'}`,
105
+ originId: r.trace_id,
106
+ timestamp: stamp(r),
107
+ }))
108
+ },
109
+ },
110
+ ]
111
+ }
112
+
113
+ function hypothesesProvider(dataDir) {
114
+ return {
115
+ domain: 'module',
116
+ name: 'hypotheses',
117
+ collect() {
118
+ return readJsonl(path.join(dataDir, 'creativity', 'creativity-hypotheses.jsonl'))
119
+ .filter((h) => h && (h.status === 'active' || h.status === 'rejected'))
120
+ .slice(-6)
121
+ .map((h) => ({
122
+ name: String(h.title || h.id).slice(0, 60),
123
+ content: `idea: ${h.idea}\nrisk: ${h.risk || '-'}`,
124
+ type: h.status === 'rejected' ? 'failure' : 'knowledge',
125
+ weight: h.status === 'rejected' ? WEIGHTS.hypothesisRejected : WEIGHTS.hypothesisActive,
126
+ origin: `hypothesis:${h.id}`,
127
+ originId: h.id,
128
+ timestamp: isoOf(h.createdAt),
129
+ }))
130
+ },
131
+ }
132
+ }
133
+
134
+ // Observer trend reports → provocation sources. The report schema has drifted
135
+ // across observer versions, so fields are read defensively with fallbacks.
136
+ function trendSources(trends) {
137
+ return (Array.isArray(trends) ? trends : []).slice(0, 4).map((t, i) => {
138
+ const title = (t && (t.title || t.name || t.date)) || `trend-${i}`
139
+ const body =
140
+ (t && t.summary) ||
141
+ (Array.isArray(t && t.themes) ? t.themes.join('; ') : '') ||
142
+ JSON.stringify(t || {}).slice(0, 400)
143
+ const key = (t && (t.date || t.id)) || String(i)
144
+ const stamp = isoOf(t && (t.generatedAt || t.date))
145
+ return {
146
+ name: String(title).slice(0, 60),
147
+ content: String(body),
148
+ type: 'provocation',
149
+ weight: WEIGHTS.trend,
150
+ origin: `observer-trends:${key}`,
151
+ ...(t && t.id ? { originId: t.id } : {}),
152
+ ...(stamp ? { timestamp: stamp } : {}),
153
+ }
154
+ })
155
+ }
156
+
157
+ // Assemble every local (+ optionally insight/observer) domain through the
158
+ // package SourceAggregator so dedup, per-domain caps and failure tolerance
159
+ // behave identically for idea.generate and the generate top-up.
160
+ function buildAutoSources({ dataDir, insights = [], trends = [] }) {
161
+ const aggregator = new SourceAggregator(4, 1)
162
+ aggregator.addProvider(memoryProvider(dataDir))
163
+ for (const provider of traceProviders(dataDir)) aggregator.addProvider(provider)
164
+ aggregator.addProvider(hypothesesProvider(dataDir))
165
+ if (Array.isArray(insights) && insights.length > 0) {
166
+ aggregator.addProvider({ domain: 'observation', name: 'insight', collect: () => sourcesFromInsights(insights) })
167
+ }
168
+ const trendList = Array.isArray(trends) ? trends : []
169
+ if (trendList.length > 0) {
170
+ aggregator.addProvider({ domain: 'external', name: 'observer-trends', collect: () => trendSources(trendList) })
171
+ }
172
+ return aggregator.build()
173
+ }
174
+
175
+ // Explicit --source entries always win: auto sources only top a set up to the
176
+ // two entries the concept mixer needs; they never replace or reorder explicit
177
+ // ones (spec Capability 2, injection point 2).
178
+ function topUpSources(explicit, auto) {
179
+ const list = Array.isArray(explicit) ? explicit.filter(Boolean) : []
180
+ if (list.length >= 2) return list
181
+ const seen = new Set(list.map((s) => s.name))
182
+ const extra = (Array.isArray(auto) ? auto : []).filter((s) => s && !seen.has(s.name))
183
+ return [...list, ...extra]
184
+ }
185
+
186
+ module.exports = { buildAutoSources, topUpSources, trendSources, memoryProvider, traceProviders, hypothesesProvider }
@@ -0,0 +1,136 @@
1
+ 'use strict'
2
+
3
+ // `mio.idea.generate` — one-shot goal-driven idea pipeline (spec Capability 1).
4
+ // Shared by the CLI (`mio idea generate`) and the MCP server (tool
5
+ // `mio.idea.generate`): each entry point only assembles entry-specific inputs
6
+ // (which store is authoritative, where local data lives) and renders the
7
+ // response this module returns.
8
+ //
9
+ // Contract:
10
+ // - source order: goal → context → constraints → grounding → local auto,
11
+ // deduplicated by `origin` (fallback name), first occurrence wins — so a
12
+ // memory that arrives both as a grounding hit and through the local
13
+ // provider contributes exactly once, the grounding copy first;
14
+ // - pairing strategy: cross-type `explore` only when two types exist, else
15
+ // the `stable` fallback (an all-knowledge set explores into ZERO combos);
16
+ // - every candidate passes evaluateNovelty against stored hypotheses: a
17
+ // near-duplicate of a REJECTED one is persisted as rejected with a reason,
18
+ // a merely-similar recent one is down-scored; drafts AND rejects both land
19
+ // in the store so later runs can dedup against them;
20
+ // - ideas are never written to Mio memory — CreativityStore only.
21
+
22
+ const { IdeaGenerator } = require('@akemi-mio/creativity/IdeaGenerator')
23
+ const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
24
+
25
+ // All > 0.7: IdeaGenerator.applyTemperature keeps such sources unconditionally
26
+ // (temperature 0.5 middle branch) — deterministic prompt, no RNG survival.
27
+ const WEIGHTS = { goal: 0.95, context: 0.9, constraint: 0.88, grounding: 0.75 }
28
+
29
+ // `queryMemory` result → grounding sources + ids. Tolerates null/empty
30
+ // (a thrown query is the caller's problem to catch; a miss is not).
31
+ function groundingFrom(queryResult) {
32
+ const results = Array.isArray(queryResult && queryResult.results) ? queryResult.results : []
33
+ const sources = results.map((r) => ({
34
+ name: `${r.kind || 'memory'}: ${String(r.content || '').slice(0, 40)}`,
35
+ content: String(r.content || ''),
36
+ type: 'feedback',
37
+ weight: WEIGHTS.grounding,
38
+ origin: `memory:${r.id}`,
39
+ originId: r.id,
40
+ ...(r.timestamp ? { timestamp: r.timestamp } : {}),
41
+ }))
42
+ return { sources, relatedMemoryIds: results.map((r) => r.id), groundedWith: results.length }
43
+ }
44
+
45
+ function namedSource(role, text, origin, weight) {
46
+ const body = String(text).trim()
47
+ return {
48
+ name: `${role}: ${body.slice(0, 40)}`,
49
+ content: body,
50
+ type: 'knowledge',
51
+ weight,
52
+ origin,
53
+ }
54
+ }
55
+
56
+ async function runIdeaGenerate(options) {
57
+ const {
58
+ goal,
59
+ context = '',
60
+ constraints = [],
61
+ numIdeas,
62
+ chatJson,
63
+ store,
64
+ groundingSources = [],
65
+ relatedMemoryIds = [],
66
+ autoSources = [],
67
+ } = options || {}
68
+
69
+ const ordered = []
70
+ if (typeof goal === 'string' && goal.trim()) ordered.push(namedSource('goal', goal, 'goal', WEIGHTS.goal))
71
+ if (typeof context === 'string' && context.trim()) ordered.push(namedSource('context', context, 'context', WEIGHTS.context))
72
+ if (Array.isArray(constraints)) {
73
+ constraints.forEach((c, i) => {
74
+ if (typeof c === 'string' && c.trim()) ordered.push(namedSource('constraint', c, `constraint:${i}`, WEIGHTS.constraint))
75
+ })
76
+ }
77
+ for (const s of [...groundingSources, ...autoSources]) {
78
+ if (s && typeof s === 'object' && s.name) ordered.push(s)
79
+ }
80
+
81
+ const seen = new Set()
82
+ const sources = []
83
+ for (const s of ordered) {
84
+ const key = s.origin || s.name
85
+ if (seen.has(key)) continue
86
+ seen.add(key)
87
+ sources.push(s)
88
+ }
89
+
90
+ if (sources.length < 2) {
91
+ return { reason: 'need at least 2 sources — add --context/--constraints, or run `mio remember` first' }
92
+ }
93
+
94
+ const pairingStrategy = new Set(sources.map((s) => s.type)).size >= 2 ? 'explore' : 'stable'
95
+ const requested = Number(numIdeas)
96
+ const maxIdeas = Number.isFinite(requested) ? Math.min(Math.max(Math.trunc(requested), 1), 3) : 3
97
+
98
+ const rotationSeed = store.getHypotheses().length
99
+ const generator = new IdeaGenerator(chatJson, 0.5, undefined, rotationSeed)
100
+ // method left undefined: HypothesisGenerator rotates techniques internally
101
+ // from rotationSeed; the chosen one comes back on hypothesis.technique.
102
+ const creativeIdeas = await generator.generateIdeas(sources, maxIdeas, pairingStrategy, [])
103
+
104
+ const generatedAt = new Date().toISOString()
105
+ const recent = store.getHypotheses().slice(-30)
106
+ const rejected = store.getHypotheses({ status: 'rejected' }).slice(-50)
107
+
108
+ const ideas = []
109
+ const persistedIds = []
110
+ for (const { hypothesis: h, experiment } of creativeIdeas) {
111
+ const verdict = evaluateNovelty(h, recent, rejected)
112
+ if (verdict.shouldReject) {
113
+ h.status = 'rejected'
114
+ h.rejectionReason = verdict.rejectReason
115
+ } else {
116
+ h.novelty = verdict.adjustedNovelty
117
+ }
118
+ const labels = new Set(Array.isArray(h.sourceLabels) ? h.sourceLabels : [])
119
+ h.provenance = {
120
+ strategy: pairingStrategy,
121
+ technique: h.technique,
122
+ relatedMemoryIds: [...relatedMemoryIds],
123
+ sources: sources
124
+ .filter((s) => labels.has(s.name))
125
+ .map((s) => ({ name: s.name, type: s.type, origin: s.origin, timestamp: s.timestamp })),
126
+ generatedAt,
127
+ }
128
+ store.addHypothesis(h)
129
+ persistedIds.push(h.id)
130
+ ideas.push({ title: h.title, hypothesis: h, experiment, novelty: h.novelty, provenance: h.provenance })
131
+ }
132
+
133
+ return { ideas, generatedAt, groundedWith: groundingSources.length, persistedIds }
134
+ }
135
+
136
+ module.exports = { runIdeaGenerate, groundingFrom }
@@ -21,6 +21,8 @@ const { createDigest } = require('../digest.js')
21
21
  const { createEvolutionReport } = require('../evolution-report.js')
22
22
 
23
23
  const { CreativityEngine, sourcesFromInsights } = require('../creativity-engine.js')
24
+ const { buildAutoSources, topUpSources } = require('../creativity-sources.js')
25
+ const { runIdeaGenerate, groundingFrom } = require('../idea-generate.js')
24
26
  // The observer and insight engines are optional dependencies. Their stores own
25
27
  // the require and expose isObserverAvailable() / isInsightAvailable() so tool
26
28
  // registration can be gated on them, and both the MCP server and the CLI share
@@ -178,6 +180,16 @@ const memoryStore = createMemoryStore({
178
180
  verifiedFilter: REUSE_STATUS_FILTERS.verified,
179
181
  })
180
182
 
183
+ // Second memory-store instance WITHOUT the shared queryLog: idea.generate's
184
+ // grounding recall is tool-side context gathering, not a user query — it must
185
+ // not append to queries.jsonl (that log exists for recall-style queries).
186
+ const groundingMemoryStore = createMemoryStore({
187
+ dataDir,
188
+ projectName,
189
+ agentId: runtimeAgentId,
190
+ verifiedFilter: REUSE_STATUS_FILTERS.verified,
191
+ })
192
+
181
193
  const digestEngine = createDigest({ home: dataDir })
182
194
 
183
195
  const {
@@ -695,6 +707,7 @@ const TOOLS = [
695
707
  properties: {
696
708
  status: { type: 'string', description: 'Filter by status: active, validated, rejected, draft.' },
697
709
  limit: { type: 'number', description: 'Max results. Default 20.' },
710
+ sort: { type: 'string', description: "Sort order: 'novelty' ranks stored hypotheses by relative noveltyScore (a dedup signal, NOT a quality score), newest 200 candidates capped. Default: newest." },
698
711
  },
699
712
  },
700
713
  },
@@ -736,6 +749,21 @@ const TOOLS = [
736
749
  },
737
750
  },
738
751
  },
752
+ {
753
+ name: 'mio.idea.generate',
754
+ description:
755
+ 'Generate up to 3 concrete, experiment-backed hypotheses from one goal. Sources are assembled locally (goal, context/constraints, Mio memory grounding, local memory/traces/stored hypotheses) and every idea is persisted to the CreativityStore with provenance — Mio memory is never written. Returns { ideas, generatedAt, groundedWith, persistedIds }, or { reason } when fewer than two sources are available.',
756
+ inputSchema: {
757
+ type: 'object',
758
+ properties: {
759
+ goal: { type: 'string', description: 'The one-line goal the ideas should pursue (required).' },
760
+ context: { type: 'string', description: 'Optional free-form background.' },
761
+ constraints: { type: 'array', items: { type: 'string' }, description: 'Optional hard constraints.' },
762
+ numIdeas: { type: 'number', description: 'How many ideas to return; clamped to 1..3, default 3.' },
763
+ },
764
+ required: ['goal'],
765
+ },
766
+ },
739
767
 
740
768
  // ═══ Observer Research Pipeline ═══
741
769
 
@@ -968,6 +996,25 @@ function observerDag(args = {}) {
968
996
  return observerStore.dag(args)
969
997
  }
970
998
 
999
+ // Local auto sources shared by generate's top-up and idea.generate — the same
1000
+ // providers the CLI assembles (spec Capability 2). Every layer degrades to []
1001
+ // so a missing optional package or an unreadable store never fails a tool call.
1002
+ function autoSources() {
1003
+ let insights = []
1004
+ let trends = []
1005
+ try {
1006
+ if (isInsightAvailable()) insights = insightStore.list({}) || []
1007
+ } catch (_) {}
1008
+ try {
1009
+ trends = observerStore.trends({ limit: 3 }) || []
1010
+ } catch (_) {}
1011
+ try {
1012
+ return buildAutoSources({ dataDir, insights, trends })
1013
+ } catch (_) {
1014
+ return []
1015
+ }
1016
+ }
1017
+
971
1018
  // `fromInsights` seeds generation from what the insight engine already noticed
972
1019
  // instead of making the caller hand-write concept sources. Only this entry
973
1020
  // point knows which store is authoritative, so the read happens here; the
@@ -975,15 +1022,24 @@ function observerDag(args = {}) {
975
1022
  // so the CLI's `--from-insights` lands on the same shapes.
976
1023
  function generationSources(args) {
977
1024
  const supplied = args.sources
978
- if (!args.fromInsights) return supplied
979
- if (!isInsightAvailable()) {
980
- throw new Error('mio.creativity.generate fromInsights requires the optional package @akemi-mio/insight')
981
- }
982
- const seeds = sourcesFromInsights(insightStore.list({}))
983
- if (seeds.length === 0) {
984
- throw new Error('mio.creativity.generate fromInsights: no stored insights yet (run `mio insight generate` first)')
1025
+ // Nothing to top up and no fromInsights: return as-is so the engine keeps
1026
+ // throwing its documented "requires sources" error for empty calls.
1027
+ if (!args.fromInsights && !(Array.isArray(supplied) && supplied.length)) return supplied
1028
+ let base = supplied
1029
+ if (args.fromInsights) {
1030
+ if (!isInsightAvailable()) {
1031
+ throw new Error('mio.creativity.generate fromInsights requires the optional package @akemi-mio/insight')
1032
+ }
1033
+ const seeds = sourcesFromInsights(insightStore.list({}))
1034
+ if (seeds.length === 0) {
1035
+ throw new Error('mio.creativity.generate fromInsights: no stored insights yet (run `mio insight generate` first)')
1036
+ }
1037
+ base = Array.isArray(supplied) ? supplied.concat(seeds) : seeds
985
1038
  }
986
- return Array.isArray(supplied) ? supplied.concat(seeds) : seeds
1039
+ // Explicit sources always win; local memory/traces/stored hypotheses only
1040
+ // top a short list up to the two entries the mixer needs (spec Capability 2,
1041
+ // injection point 2 — the MCP twin of `mio creativity generate`).
1042
+ return topUpSources(Array.isArray(base) ? base : [], autoSources())
987
1043
  }
988
1044
 
989
1045
  async function callTool(name, args = {}) {
@@ -1060,6 +1116,26 @@ async function callTool(name, args = {}) {
1060
1116
  return creativityEngine.generate(generationSources(args), args.strategy)
1061
1117
  case 'mio.creativity.ferment':
1062
1118
  return creativityEngine.ferment(args.limit)
1119
+ case 'mio.idea.generate': {
1120
+ const goal = typeof args.goal === 'string' ? args.goal.trim() : ''
1121
+ if (!goal) throw new Error('mio.idea.generate requires goal: a non-empty string')
1122
+ let grounding = { sources: [], relatedMemoryIds: [], groundedWith: 0 }
1123
+ try {
1124
+ const qr = await groundingMemoryStore.queryMemory({ query: goal, limit: 3, scope: 'all' })
1125
+ grounding = groundingFrom(qr)
1126
+ } catch (_) {}
1127
+ return runIdeaGenerate({
1128
+ goal,
1129
+ context: typeof args.context === 'string' ? args.context : '',
1130
+ constraints: Array.isArray(args.constraints) ? args.constraints.filter((c) => typeof c === 'string') : [],
1131
+ numIdeas: args.numIdeas,
1132
+ chatJson,
1133
+ store: creativityEngine.store,
1134
+ groundingSources: grounding.sources,
1135
+ relatedMemoryIds: grounding.relatedMemoryIds,
1136
+ autoSources: autoSources(),
1137
+ })
1138
+ }
1063
1139
  case 'mio.insight.status':
1064
1140
  return insightStatus()
1065
1141
  case 'mio.insight.list':