mio-agent-runtime 0.14.1 → 0.15.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
@@ -53,10 +53,11 @@ mio memory forget --ids a,b PERMANENTLY delete records (--yes required; writ
53
53
  mio memory merge --ids a,b Merge duplicate records into one survivor (--keep/--allow-divergent; --yes required)
54
54
  mio memory migrate --ids a,b --scope global|project Move records between the project and global layers
55
55
  mio policy check "<action>" Check the historical risk of an action before running it (--project; reads global MIO_HOME)
56
- mio creativity status Show creativity hypothesis counts and recent top ideas (reads global MIO_HOME)
56
+ mio creativity status Show creativity hypothesis counts, recent top ideas, adoption join (reads global MIO_HOME)
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 creativity adopt Record an adoption event for a stored hypothesis (writes traces only)
60
61
  mio idea generate --goal "..." Grounded idea pipeline: auto sources + novelty gate, persisted (calls an LLM)
61
62
  mio insight status Insight counts: total, reported, unreported, high-value (needs @akemi-mio/insight)
62
63
  mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
@@ -102,7 +103,7 @@ mio --version Print the version and exit (-V)
102
103
 
103
104
  ## MCP 工具
104
105
 
105
- MCP 服务端在 5 大域共暴露 51 个工具:
106
+ MCP 服务端在 5 大域共暴露 52 个工具:
106
107
 
107
108
  ### 记忆(11)
108
109
  `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`
@@ -115,8 +116,8 @@ MCP 服务端在 5 大域共暴露 51 个工具:
115
116
  ### 洞察自省(4)
116
117
  `mio.insight.status` · `mio.insight.list` · `mio.insight.mark_reported` · `mio.insight.generate`
117
118
 
118
- ### 创意引擎(5)
119
- `mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status` · `mio.idea.generate`
119
+ ### 创意引擎(6)
120
+ `mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.adopt` · `mio.creativity.list` · `mio.creativity.status` · `mio.idea.generate`
120
121
 
121
122
  ### 任务、Agent 与演化(16)
122
123
  `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`
@@ -389,6 +390,7 @@ mio creativity list --json
389
390
  Creativity engine:
390
391
  hypotheses: 5 combos: 3 experiments: 1
391
392
  active: 2 validated: 1 rejected: 1 draft: 1
393
+ adoption: 2 adopted (events=2, tags=2) — derived join, not a metric
392
394
 
393
395
  Recent top ideas:
394
396
  - Plugin architecture (novelty=80 feasibility=70 impact=90 score=240)
@@ -403,6 +405,32 @@ Recent top ideas:
403
405
  - **空存储是正常状态。** 引擎运行之前,`status` 报告全零计数,`list` 显示 "No hypotheses match." 两者都是有效输出,不是错误。
404
406
  - **缓存会在文件变化时失效。** 三个 JSONL 的读取按 `(size, mtime)` 校验,因此另一个进程写入后长驻的 MCP 服务端无需重启就能看到——与 `server/insight-store.js` 同一套守卫。
405
407
 
408
+ ### 采用证据链(adopt / status / list)
409
+
410
+ 假设的「采用」**不在存储里**,而是三轴分离的证据链——store 只管评审,采用是假设与外部世界的关系:
411
+
412
+ - **评审轴(`creativity-hypotheses.jsonl`)**:`draft → active → validated / rejected`,假说自身的生命周期,由 `generate` / `ferment` 推进。
413
+ - **声称轴(`traces.jsonl`)**:`mio.creativity.adopt`(MCP `mio.creativity.adopt`)追加 `creativity.adopt` 事件,`payload.hypothesisId` 即声称;只写事件、绝不碰 memory,校验失败零写入。
414
+ - **证据轴(`memory.jsonl`)**:`mio memory record ... --hypothesis-id <uuid>`(MCP 参数 `hypothesisId`)把 `hypothesis:<uuid>` 注入记录 tags;`mio memory query --tags hypothesis:<uuid>` 即按标签召回。`AGENTS.md` 规范:**采用假设做决策时 `memory.record` 必带 `hypothesisId`**——这是把决策与假设挂上钩的唯一写入口。
415
+
416
+ `status` / `list` 在**读侧**把两轴 join 成派生计数(`mio.creativity.status` 返回):
417
+
418
+ ```json
419
+ "adoption": {
420
+ "adopted": 2, "claimed": 3, "evidenced": 2,
421
+ "metric": false,
422
+ "note": "derived join of creativity.adopt events and hypothesis:<id> memory tags — informational; no rate or threshold by design",
423
+ "sources": {
424
+ "events": { "file": "traces.jsonl", "durable": true },
425
+ "memory": { "file": "memory.jsonl", "durable": true }
426
+ }
427
+ }
428
+ ```
429
+
430
+ - `claimed` / `evidenced` 是两轴各自的 **distinct id 原始计数**(可以含已不在存储中的悬空 id);`adopted` = 两集合并集与存储假设 id 的交集——只有它回答「**存下来的**假设里哪些被采用过」。原始与 join 并报,悬空证据因此可见而不是被静默吞掉。
431
+ - **`metric: false` 是承重字段**:这是信息性派生 join,**不是** ADR-017 评估指标——没有 adoption rate、没有阈值。`sources` 照 `routeAdoption` 的模式注明每个轴的背书文件与 durable 性,于是「0 adopted(真没人用)」与「证据文件没留存(不可测)」能区分开。
432
+ - `list` 每行带 `adopted: boolean`,文本模式在状态后显示 `[adopted]` 标记;JSON 输出即引擎结果,MCP 与 CLI 两侧天然一致。
433
+
406
434
  ### 生成与发酵(`generate` / `ferment`)
407
435
 
408
436
  这两个子命令会调用 LLM,`mio creativity` 同样支持它们——用的是**共享的 LLM 客户端**
package/bin/mio.js CHANGED
@@ -23,7 +23,7 @@ const { createEvolutionCutoverTools } = require('../server/evolution-cutover.js'
23
23
  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
- const { CreativityEngine, sourcesFromInsights } = require('../server/creativity-engine.js')
26
+ const { CreativityEngine, sourcesFromInsights, adoptHypothesis } = require('../server/creativity-engine.js')
27
27
  const { buildAutoSources, topUpSources } = require('../server/creativity-sources.js')
28
28
  const { runIdeaGenerate, groundingFrom } = require('../server/idea-generate.js')
29
29
  const { createAgentStore } = require('../server/agent-store.js')
@@ -678,18 +678,22 @@ function pruneCommand(args, useJson) {
678
678
  function rememberCommand(args, useJson) {
679
679
  const content = args[1]
680
680
  if (!content) {
681
- console.error('Usage: mio remember "<content>" [--kind decision|context|problem|note] [--tags a,b] [--scope project|global] [--project name] [--json]')
681
+ console.error('Usage: mio remember "<content>" [--kind decision|context|problem|note] [--tags a,b] [--hypothesis-id id] [--scope project|global] [--project name] [--json]')
682
682
  process.exitCode = 1
683
683
  return
684
684
  }
685
685
  const flags = args.slice(2)
686
686
  const store = cliMemoryStore()
687
+ // optionValue returns the next token even if it is another flag; a bare
688
+ // trailing `--hypothesis-id` must not swallow `--json` as a "uuid".
689
+ const hypothesisId = optionValue(flags, '--hypothesis-id')
687
690
  let record
688
691
  try {
689
692
  record = store.recordMemory({
690
693
  content,
691
694
  kind: optionValue(flags, '--kind'),
692
695
  tags: splitTagsOption(flags, '--tags'),
696
+ hypothesisId: hypothesisId && !hypothesisId.startsWith('--') ? hypothesisId : undefined,
693
697
  scope: optionValue(flags, '--scope'),
694
698
  project: optionValue(flags, '--project'),
695
699
  source: 'cli',
@@ -2066,11 +2070,12 @@ function observerCommand(args, useJson) {
2066
2070
 
2067
2071
  function creativityUsage(write = console.log) {
2068
2072
  write(`Usage:
2069
- mio creativity status Show hypothesis counts and recent top ideas
2073
+ mio creativity status Show hypothesis counts, recent top ideas, adoption join
2070
2074
  mio creativity list List hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
2071
2075
 
2072
2076
  mio creativity generate Generate hypotheses by combining sources (needs 2+ --source, calls an LLM)
2073
2077
  mio creativity ferment Review/refine active hypotheses (calls an LLM)
2078
+ mio creativity adopt Record an adoption event for a stored hypothesis (writes traces only)
2074
2079
 
2075
2080
  Options:
2076
2081
  --status name Filter list by status
@@ -2098,6 +2103,8 @@ function printCreativityStatus(result) {
2098
2103
  console.log('Creativity engine:')
2099
2104
  console.log(` hypotheses: ${result.hypotheses} combos: ${result.combos} experiments: ${result.experiments}`)
2100
2105
  console.log(` active: ${result.active} validated: ${result.validated} rejected: ${result.rejected} draft: ${result.draft}`)
2106
+ const adoption = result.adoption || { adopted: 0, claimed: 0, evidenced: 0 }
2107
+ console.log(` adoption: ${adoption.adopted} adopted (events=${adoption.claimed} tags=${adoption.evidenced}) — derived join, not a metric`)
2101
2108
  if (result.recentIdeas.length === 0) {
2102
2109
  console.log('No hypotheses yet.')
2103
2110
  return
@@ -2118,7 +2125,7 @@ function printCreativityList(items) {
2118
2125
  items.forEach((h, index) => {
2119
2126
  const when = h.createdAt ? new Date(h.createdAt).toISOString().slice(0, 19).replace('T', ' ') : ''
2120
2127
  const labels = Array.isArray(h.sourceLabels) && h.sourceLabels.length > 0 ? h.sourceLabels.join(' + ') : '—'
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}` : ''})`)
2128
+ console.log(`${index + 1}. [${h.status}]${h.adopted ? ' [adopted]' : ''} ${h.title} (N=${h.novelty} F=${h.feasibility} I=${h.impact} score=${h.score}${h.noveltyScore !== undefined ? ` noveltyScore=${h.noveltyScore}` : ''})`)
2122
2129
  console.log(` ${labels} | ${when} | ${h.id}`)
2123
2130
  if (h.fermentCount) console.log(` fermented ${h.fermentCount}x`)
2124
2131
  })
@@ -2296,10 +2303,45 @@ function creativityFermentCommand(args, useJson) {
2296
2303
  })
2297
2304
  }
2298
2305
 
2306
+ // mio.creativity.adopt from the terminal: same shared adoptHypothesis the MCP
2307
+ // server calls, same enforcement (id must exist; --memory-id must already
2308
+ // carry the hypothesis:<id> tag), event written to MIO_HOME/traces.jsonl.
2309
+ function creativityAdoptCommand(args, useJson) {
2310
+ const flags = args.slice(2)
2311
+ const hypothesisId = flags[0] && !flags[0].startsWith('--') ? String(flags[0]).trim() : ''
2312
+ if (!hypothesisId) {
2313
+ console.error('mio creativity adopt requires <hypothesisId>')
2314
+ console.error('Usage: mio creativity adopt <hypothesisId> [--memory-id id] [--task-id id] [--note text] [--json]')
2315
+ process.exitCode = 1
2316
+ return
2317
+ }
2318
+
2319
+ let result
2320
+ try {
2321
+ result = adoptHypothesis(
2322
+ cliCreativityEngine(),
2323
+ {
2324
+ hypothesisId,
2325
+ memoryId: optionValue(flags, '--memory-id'),
2326
+ taskId: optionValue(flags, '--task-id'),
2327
+ note: optionValue(flags, '--note'),
2328
+ },
2329
+ cliTaskStore().ingestObservation,
2330
+ MIO_HOME
2331
+ )
2332
+ } catch (error) {
2333
+ console.error(error.message || error)
2334
+ process.exitCode = 1
2335
+ return
2336
+ }
2337
+ if (useJson) return jsonOrText(result, true)
2338
+ console.log(`Adopted hypothesis ${result.hypothesisId}`)
2339
+ console.log(` event: ${result.event && result.event.trace_id ? result.event.trace_id : 'creativity.adopt'}`)
2340
+ }
2341
+
2299
2342
  function ideaUsage() {
2300
2343
  console.error('usage: mio idea generate --goal "one line" [--context "..."] [--constraint "..."] [--num N] [--json]')
2301
2344
  }
2302
-
2303
2345
  async function ideaGenerateCommand(args, useJson) {
2304
2346
  const flags = args.slice(2)
2305
2347
  const goal = (optionValue(flags, '--goal') || '').trim()
@@ -2390,6 +2432,7 @@ function creativityCommand(args, useJson) {
2390
2432
  }
2391
2433
  if (sub === 'generate') return creativityGenerateCommand(args, useJson)
2392
2434
  if (sub === 'ferment') return creativityFermentCommand(args, useJson)
2435
+ if (sub === 'adopt') return creativityAdoptCommand(args, useJson)
2393
2436
 
2394
2437
  if (!['status', 'list'].includes(sub)) {
2395
2438
  console.error(`Unknown creativity subcommand: ${sub}`)
@@ -3492,10 +3535,11 @@ Usage:
3492
3535
  mio experience list List experience reuse (--status pending|confirmed|verified)
3493
3536
  mio experience confirm --ids a,b Confirm auto-claimed reuse (bulk supported)
3494
3537
  mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse
3495
- mio creativity status Show creativity hypothesis counts and recent top ideas
3538
+ mio creativity status Show creativity hypothesis counts, recent top ideas, adoption join
3496
3539
  mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
3497
3540
  mio creativity generate Generate hypotheses from 2+ --source "name|content" (or --from-insights; calls an LLM)
3498
3541
  mio creativity ferment Review and refine active hypotheses (calls an LLM)
3542
+ mio creativity adopt Record an adoption event for a stored hypothesis (writes traces only)
3499
3543
  mio idea generate --goal "..." Grounded idea pipeline: auto sources, novelty gate, persisted (calls an LLM)
3500
3544
  mio insight status Insight counts: total, reported, unreported, high-value
3501
3545
  mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -34,7 +34,7 @@
34
34
  "@akemi-mio/evolution-strategy": "^0.1.0",
35
35
  "@akemi-mio/evolution-safety": "^0.1.0",
36
36
  "@akemi-mio/evolution-scheduler": "^0.1.0",
37
- "@akemi-mio/observer": "^0.1.4",
37
+ "@akemi-mio/observer": "^0.1.5",
38
38
  "@akemi-mio/insight": "^0.1.1",
39
39
  "@akemi-mio/creativity": "^0.1.0"
40
40
  },
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
  const crypto = require('crypto')
6
+ const { readJsonl } = require('./memory-store.js')
6
7
  const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
7
8
 
8
9
  // Cache validity key for a JSONL file: a (size, mtimeMs) pair, the same
@@ -257,8 +258,55 @@ function listRow(h) {
257
258
  }
258
259
  }
259
260
 
261
+ // --- read-side adoption join (idea b19bada4, design discussion 1d2e22e4 D3) ---
262
+ // The store never gains an `adopted` status (D1/D4, see adoptHypothesis at the
263
+ // bottom of this file): adoption is joined at READ time from the two evidence
264
+ // axes the write side owns -- an append-only `creativity.adopt` trace event
265
+ // (payload.hypothesisId = the CLAIM, written by mio.creativity.adopt) and a
266
+ // `hypothesis:<id>` tag on a memory record (the EVIDENCE, injected by
267
+ // mio.memory.record's hypothesisId parameter). The engine sits in
268
+ // <dataRoot>/creativity on both hosts (MCP: path.join(dataDir, 'creativity');
269
+ // CLI: path.join(MIO_HOME, 'creativity')), so the two evidence files are one
270
+ // dirname up -- the same dataRoot convention adoptHypothesis documents.
271
+ // Missing files are fine: readJsonl returns [] for them.
272
+ //
273
+ // The counts are informational joins, NOT ADR-017 metrics: no rate and no
274
+ // threshold by design (D3 -- adoption rate was explicitly out of scope).
275
+ // `sources` follows the routeAdoption pattern (evaluation-store.js): names
276
+ // each axis' backing file and that it is durable, so a reader can tell
277
+ // "0 adopted" apart from "evidence not kept". claimed/evidenced are raw
278
+ // distinct ids (they can include ids no longer in the store); adopted is the
279
+ // inner join against stored hypotheses, the only count that answers "which
280
+ // surviving hypotheses were adopted".
281
+ const ADOPTION_TAG_PREFIX = 'hypothesis:'
282
+
283
+ const ADOPTION_SOURCES = Object.freeze({
284
+ events: { file: 'traces.jsonl', durable: true },
285
+ memory: { file: 'memory.jsonl', durable: true },
286
+ })
287
+
288
+ function readAdoptionEvidence(rootDir) {
289
+ const claimed = new Set()
290
+ for (const record of readJsonl(path.join(rootDir, 'traces.jsonl'))) {
291
+ if (!record || record.event_type !== 'creativity.adopt') continue
292
+ const id = record.payload && record.payload.hypothesisId
293
+ if (id) claimed.add(String(id))
294
+ }
295
+ const evidenced = new Set()
296
+ for (const record of readJsonl(path.join(rootDir, 'memory.jsonl'))) {
297
+ if (!record || !Array.isArray(record.tags)) continue
298
+ for (const tag of record.tags) {
299
+ if (typeof tag !== 'string' || !tag.startsWith(ADOPTION_TAG_PREFIX)) continue
300
+ const id = tag.slice(ADOPTION_TAG_PREFIX.length).trim()
301
+ if (id) evidenced.add(id)
302
+ }
303
+ }
304
+ return { claimed, evidenced }
305
+ }
306
+
260
307
  class CreativityEngine {
261
308
  constructor(dataDir, chatJson) {
309
+ this.dataDir = dataDir
262
310
  this.store = new CreativityStore(dataDir)
263
311
  this.chatJson = chatJson
264
312
  this.strategyIndex = 0
@@ -510,6 +558,20 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
510
558
  return { fermented: results.length, results, errors }
511
559
  }
512
560
 
561
+ // Fresh per call: both evidence files are append-only and a long-lived MCP
562
+ // server must see new adopt events / memory tags without a restart (same
563
+ // rationale as the insight-store mtime cache -- minus the cache, since this
564
+ // is two small file reads on a read-only report path).
565
+ _adoption() {
566
+ const { claimed, evidenced } = readAdoptionEvidence(path.dirname(this.dataDir))
567
+ const storedIds = new Set(this.store.getHypotheses().map((h) => h.id))
568
+ const adoptedIds = new Set()
569
+ for (const id of [...claimed, ...evidenced]) {
570
+ if (storedIds.has(id)) adoptedIds.add(id)
571
+ }
572
+ return { claimed, evidenced, adoptedIds }
573
+ }
574
+
513
575
  status() {
514
576
  const count = this.store.count()
515
577
  const active = this.store.getHypotheses({ status: 'active' })
@@ -518,6 +580,7 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
518
580
  // draft is a real state -- ferment() reviews draft + active -- so hiding it
519
581
  // here made `status` and `list --status draft` disagree.
520
582
  const draft = this.store.getHypotheses({ status: 'draft' })
583
+ const adoption = this._adoption()
521
584
 
522
585
  return {
523
586
  ...count,
@@ -525,6 +588,19 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
525
588
  validated: validated.length,
526
589
  rejected: rejected.length,
527
590
  draft: draft.length,
591
+ // Derived join of the two evidence axes (see readAdoptionEvidence).
592
+ // metric:false is load-bearing: this is NOT an ADR-017 evaluation metric,
593
+ // so no rate and no threshold are reported -- adoption rate was explicitly
594
+ // out of scope (D3). sources names the backing files, routeAdoption style.
595
+ adoption: {
596
+ adopted: adoption.adoptedIds.size,
597
+ claimed: adoption.claimed.size,
598
+ evidenced: adoption.evidenced.size,
599
+ metric: false,
600
+ note:
601
+ 'derived join of creativity.adopt events and hypothesis:<id> memory tags — informational; no rate or threshold by design',
602
+ sources: ADOPTION_SOURCES,
603
+ },
528
604
  recentIdeas: active.slice(-5).map(h => ({
529
605
  id: h.id,
530
606
  title: h.title,
@@ -544,6 +620,11 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
544
620
  // result. `limit: 0` (and only 0) opts back into "everything".
545
621
  list(opts = {}) {
546
622
  const limit = opts.limit === undefined ? 20 : opts.limit
623
+ // Same read-side join as status(): every row carries `adopted` so the two
624
+ // surfaces cannot disagree (the CLI/MCP entry points already share this
625
+ // engine). listRow() stays the pure store-row shape; decoration is here.
626
+ const adoption = this._adoption()
627
+ const decorate = (row) => ({ ...row, adopted: adoption.adoptedIds.has(row.id) })
547
628
  if (opts.sort === 'novelty') {
548
629
  // Relative novelty ranking (read-only, nothing written back). Pairwise
549
630
  // Jaccard is O(n²) — bound candidates to the newest 200 (spec Risk 3).
@@ -554,9 +635,13 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
554
635
  return { ...listRow(h), noveltyScore: verdict.adjustedNovelty }
555
636
  })
556
637
  rows.sort((a, b) => b.noveltyScore - a.noveltyScore || (b.createdAt || 0) - (a.createdAt || 0))
557
- return limit === 0 ? rows : rows.slice(0, limit)
638
+ const decorated = rows.map(decorate)
639
+ return limit === 0 ? decorated : decorated.slice(0, limit)
558
640
  }
559
- return this.store.getHypotheses({ status: opts.status, limit }).map(listRow)
641
+ return this.store
642
+ .getHypotheses({ status: opts.status, limit })
643
+ .map(listRow)
644
+ .map(decorate)
560
645
  }
561
646
 
562
647
  _buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
@@ -605,4 +690,69 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
605
690
  }
606
691
  }
607
692
 
608
- module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights, isoOf }
693
+ // Adoption evidence (idea b19bada4, design discussion 1d2e22e4 D1/D4): the
694
+ // store never gains an `adopted` status -- adoption is a relation between a
695
+ // stored hypothesis and the outside world, not an attribute of the hypothesis,
696
+ // so it lives as an append-only `creativity.adopt` trace event, optionally
697
+ // cross-checked against a memory record carrying the `hypothesis:<id>` tag.
698
+ // The host (MCP or CLI) passes its own ingestObservation so both entry points
699
+ // write byte-identical events; dataRoot is the directory holding memory.jsonl
700
+ // and traces.jsonl -- the engine sits in <dataRoot>/creativity on both hosts
701
+ // (see cliCreativityEngine in bin/mio.js). Every validation failure happens
702
+ // before ingest, so a rejected call never pollutes the event stream. This
703
+ // function writes traces only: it never touches memory.jsonl (the
704
+ // generate/ferment write nothing to Memory boundary extends here).
705
+ function adoptHypothesis(engine, args = {}, ingest, dataRoot) {
706
+ const hypothesisId = String(args.hypothesisId || '').trim()
707
+ if (!hypothesisId) {
708
+ throw new Error('mio.creativity.adopt requires hypothesisId: a stored hypothesis UUID')
709
+ }
710
+ if (!engine.store.getHypotheses().some((h) => h.id === hypothesisId)) {
711
+ throw new Error(`mio.creativity.adopt: no stored hypothesis with id "${hypothesisId}"`)
712
+ }
713
+ const memoryId = String(args.memoryId || '').trim()
714
+ if (memoryId) {
715
+ const memoryPath = path.join(dataRoot, 'memory.jsonl')
716
+ const records = fs.existsSync(memoryPath)
717
+ ? fs
718
+ .readFileSync(memoryPath, 'utf-8')
719
+ .split('\n')
720
+ .filter(Boolean)
721
+ .map((line) => {
722
+ try { return JSON.parse(line) } catch { return null }
723
+ })
724
+ .filter(Boolean)
725
+ : []
726
+ const record = records.find((r) => r && r.id === memoryId)
727
+ if (!record) {
728
+ throw new Error(`mio.creativity.adopt: no memory record with id "${memoryId}"`)
729
+ }
730
+ const tag = `hypothesis:${hypothesisId}`
731
+ if (!Array.isArray(record.tags) || !record.tags.includes(tag)) {
732
+ throw new Error(`mio.creativity.adopt: memory record "${memoryId}" is missing tag "${tag}"`)
733
+ }
734
+ }
735
+ if (typeof ingest !== 'function') {
736
+ throw new Error('mio.creativity.adopt: ingest is not available on this host')
737
+ }
738
+ const taskId = String(args.taskId || '').trim()
739
+ const note = String(args.note || '').trim()
740
+ const result = ingest({
741
+ trace_id: `creativity-adopt:${hypothesisId}:${Date.now()}`,
742
+ event_type: 'creativity.adopt',
743
+ outcome: 'success',
744
+ payload: {
745
+ hypothesisId,
746
+ ...(memoryId ? { memoryId } : {}),
747
+ ...(taskId ? { taskId } : {}),
748
+ ...(note ? { note } : {}),
749
+ },
750
+ })
751
+ return {
752
+ recorded: true,
753
+ hypothesisId,
754
+ event: (result && result.event) || result,
755
+ }
756
+ }
757
+
758
+ module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights, isoOf, adoptHypothesis }
@@ -518,12 +518,23 @@ function createMemoryStore(options = {}) {
518
518
  const content = String(args.content || '').trim()
519
519
  if (!content) throw new Error('memory.record requires a non-empty content')
520
520
  const scope = normalizeScope(args.scope)
521
+ // Adoption evidence (idea b19bada4, task 2261a5f4): hypothesisId is pure
522
+ // sugar for the exact tag `mio.creativity.adopt` enforces — the record
523
+ // joins the creativity hypothesis through the same `hypothesis:<uuid>`
524
+ // tag the tags AND-filter already searches. Omitted, the record keeps
525
+ // the exact legacy shape: no extra field, tags byte-identical.
526
+ const hypothesisId = String(args.hypothesisId || '').trim()
527
+ const tags = normalizeTags(args.tags)
528
+ if (hypothesisId) {
529
+ const tag = `hypothesis:${hypothesisId}`
530
+ if (!tags.includes(tag)) tags.push(tag)
531
+ }
521
532
  const record = {
522
533
  id: createId('mem'),
523
534
  timestamp: new Date().toISOString(),
524
535
  kind: args.kind || 'note',
525
536
  content,
526
- tags: normalizeTags(args.tags),
537
+ tags,
527
538
  project: scope === 'global' ? null : args.project || projectName(),
528
539
  scope,
529
540
  source: args.source || agentId() || 'mcp',
@@ -20,7 +20,7 @@ const { createSubscriptionStore } = require('../subscription-store.js')
20
20
  const { createDigest } = require('../digest.js')
21
21
  const { createEvolutionReport } = require('../evolution-report.js')
22
22
 
23
- const { CreativityEngine, sourcesFromInsights } = require('../creativity-engine.js')
23
+ const { CreativityEngine, sourcesFromInsights, adoptHypothesis } = require('../creativity-engine.js')
24
24
  const { buildAutoSources, topUpSources } = require('../creativity-sources.js')
25
25
  const { runIdeaGenerate, groundingFrom } = require('../idea-generate.js')
26
26
  // The observer and insight engines are optional dependencies. Their stores own
@@ -351,6 +351,11 @@ const TOOLS = [
351
351
  content: { type: 'string', description: 'Memory content.' },
352
352
  kind: { type: 'string', description: 'Memory kind, such as decision, context, or problem.' },
353
353
  tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
354
+ hypothesisId: {
355
+ type: 'string',
356
+ description:
357
+ 'Optional creativity hypothesis UUID. Injects the tag "hypothesis:<id>" so this record joins that hypothesis (the tag mio.creativity.adopt enforces); search it back via memory.query tags.',
358
+ },
354
359
  project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
355
360
  scope: { type: 'string', description: 'Record scope: project (default) or global.' },
356
361
  source: { type: 'string', description: 'Recording source. Defaults to mcp.' },
@@ -696,12 +701,12 @@ const TOOLS = [
696
701
  },
697
702
  {
698
703
  name: 'mio.creativity.status',
699
- description: 'Show creativity engine status: hypothesis counts, active/validated/rejected/draft, recent top ideas.',
704
+ description: 'Show creativity engine status: hypothesis counts, active/validated/rejected/draft, recent top ideas, plus the adoption evidence join — adopted/claimed/evidenced counts with sources (informational, NOT a metric; derived from creativity.adopt trace events and hypothesis:<id> memory tags).',
700
705
  inputSchema: { type: 'object', properties: {} },
701
706
  },
702
707
  {
703
708
  name: 'mio.creativity.list',
704
- description: 'List creativity hypotheses with optional status filter. Newest 20 by default; pass limit: 0 for everything.',
709
+ description: 'List creativity hypotheses with optional status filter. Newest 20 by default; pass limit: 0 for everything. Each row carries adopted: true when a creativity.adopt event or a hypothesis:<id> memory tag exists for its id (read-side evidence join, informational).',
705
710
  inputSchema: {
706
711
  type: 'object',
707
712
  properties: {
@@ -749,6 +754,21 @@ const TOOLS = [
749
754
  },
750
755
  },
751
756
  },
757
+ {
758
+ name: 'mio.creativity.adopt',
759
+ description:
760
+ 'Record that a stored creativity hypothesis was adopted: validates the hypothesis id, then appends a creativity.adopt trace event (who/when/what). Optionally enforces that the referenced memory record carries the hypothesis:<id> tag. Writes traces only — never Mio memory, never hypothesis status.',
761
+ inputSchema: {
762
+ type: 'object',
763
+ properties: {
764
+ hypothesisId: { type: 'string', description: 'UUID of a stored hypothesis (creativity-hypotheses.jsonl). Must exist or the call fails.' },
765
+ memoryId: { type: 'string', description: 'Optional memory record id. If given, the record must exist and carry the tag "hypothesis:<hypothesisId>" or the call fails.' },
766
+ taskId: { type: 'string', description: 'Optional task id this adoption feeds.' },
767
+ note: { type: 'string', description: 'Optional short rationale for the adoption.' },
768
+ },
769
+ required: ['hypothesisId'],
770
+ },
771
+ },
752
772
  {
753
773
  name: 'mio.idea.generate',
754
774
  description:
@@ -1116,6 +1136,8 @@ async function callTool(name, args = {}) {
1116
1136
  return creativityEngine.generate(generationSources(args), args.strategy)
1117
1137
  case 'mio.creativity.ferment':
1118
1138
  return creativityEngine.ferment(args.limit)
1139
+ case 'mio.creativity.adopt':
1140
+ return adoptHypothesis(creativityEngine, args, ingestObservation, dataDir)
1119
1141
  case 'mio.idea.generate': {
1120
1142
  const goal = typeof args.goal === 'string' ? args.goal.trim() : ''
1121
1143
  if (!goal) throw new Error('mio.idea.generate requires goal: a non-empty string')