mio-agent-runtime 0.5.22 → 0.6.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/bin/mio.js CHANGED
@@ -21,8 +21,21 @@ const observer = require('../observe/observer.js')
21
21
  const { getEvolutionStatus, formatEvolutionStatusText } = require('../server/runtime-modules.js')
22
22
  const { createEvolutionCutoverTools } = require('../server/evolution-cutover.js')
23
23
  const { createMemoryStore } = require('../server/memory-store.js')
24
+ const { createExperienceStore } = require('../server/experience-store.js')
25
+ const { createPolicyStore } = require('../server/policy-store.js')
26
+ const { CreativityEngine } = require('../server/creativity-engine.js')
27
+ const { createAgentStore } = require('../server/agent-store.js')
28
+ const { createInsightStore } = require('../server/insight-store.js')
29
+ const { createObserverStore } = require('../server/observer-store.js')
30
+ const { loadPhase0, renderPhase0Markdown } = require('../server/mio-intelligence-mcp/phase0.js')
31
+ const { listHostCapabilities } = require('../server/host-capabilities.js')
32
+ const { createTaskStore } = require('../server/task-store.js')
33
+ const { chatJson, llmConfig, isLlmConfigured } = require('../server/llm-client.js')
34
+ const { createQueryLog } = require('../server/query-log.js')
35
+ const { createSubscriptionStore, subscribeKey } = require('../server/subscription-store.js')
24
36
  const { createRetention } = require('../server/retention.js')
25
37
  const { createDigest } = require('../server/digest.js')
38
+ const { createEvolutionReport, formatEvolutionReportText } = require('../server/evolution-report.js')
26
39
 
27
40
  function appendJsonl(file, value) {
28
41
  fs.mkdirSync(path.dirname(file), { recursive: true })
@@ -96,6 +109,39 @@ function parseNumberOption(args, name) {
96
109
  return value
97
110
  }
98
111
 
112
+ // mio evolution report shares one implementation with mio.evolution.report
113
+ // (see ../server/evolution-report.js), so CLI and MCP cannot report different numbers.
114
+ function cliEvolutionReport() {
115
+ return createEvolutionReport({ dataDir: MIO_HOME, projectName })
116
+ }
117
+
118
+ const EVOLUTION_REPORT_PERIODS = new Set(['24h', '7d', '30d', 'all'])
119
+
120
+ function evolutionReportCommand(args, useJson) {
121
+ const flags = args.slice(2)
122
+ const period = optionValue(flags, '--period')
123
+ if (period !== undefined && !EVOLUTION_REPORT_PERIODS.has(period)) {
124
+ console.error(`--period must be one of: ${[...EVOLUTION_REPORT_PERIODS].join(', ')}`)
125
+ process.exitCode = 1
126
+ return
127
+ }
128
+
129
+ let result
130
+ try {
131
+ result = cliEvolutionReport().report({
132
+ project: optionValue(flags, '--project'),
133
+ period,
134
+ })
135
+ } catch (error) {
136
+ console.error(error.message || error)
137
+ process.exitCode = 1
138
+ return
139
+ }
140
+
141
+ if (useJson) return jsonOrText(result, true)
142
+ console.log(formatEvolutionReportText(result))
143
+ }
144
+
99
145
  function evolutionStatus(useJson) {
100
146
  const payload = getEvolutionStatus()
101
147
  if (useJson) {
@@ -148,6 +194,7 @@ function printDualWriteRecord(record, useJson) {
148
194
 
149
195
  async function evolutionCommand(args, useJson) {
150
196
  if (args[1] === 'status') return evolutionStatus(useJson)
197
+ if (args[1] === 'report') return evolutionReportCommand(args, useJson)
151
198
  try {
152
199
  if (args[1] === 'shadow' && args[2] === 'record') {
153
200
  return printShadowRecord(
@@ -218,7 +265,7 @@ async function evolutionCommand(args, useJson) {
218
265
  process.exitCode = 1
219
266
  return
220
267
  }
221
- console.error('Usage: mio evolution status|shadow record|dual-write record|cutover readiness|cutover apply|authority plan|migration plan')
268
+ console.error('Usage: mio evolution status|report|shadow record|dual-write record|cutover readiness|cutover apply|authority plan|migration plan')
222
269
  process.exitCode = 1
223
270
  }
224
271
 
@@ -238,6 +285,54 @@ function cliMemoryStore() {
238
285
  })
239
286
  }
240
287
 
288
+ // policy.check deliberately reads the *global* MIO_HOME rather than a
289
+ // per-project directory, so its history matches `mio recall` and `mio traces`.
290
+ // It shares the memory store's tokenizer and scoring, so risk evidence and
291
+ // related-memory ranking line up with `mio.memory.query`.
292
+ function cliPolicyStore() {
293
+ const memoryStore = cliMemoryStore()
294
+ return createPolicyStore({
295
+ dataDir: MIO_HOME,
296
+ projectName,
297
+ memoryStore,
298
+ })
299
+ }
300
+
301
+ // The creativity engine is the single implementation behind mio.creativity.*;
302
+ // the CLI points it at the global MIO_HOME so a terminal `mio creativity list`
303
+ // sees the same hypotheses the MCP server would. generate/ferment need an LLM,
304
+ // so they use the shared client -- configured with LLM_API_URL / LLM_KEY /
305
+ // LLM_CHAT_MODEL, the same environment the MCP server reads. A local Ollama
306
+ // works: LLM_API_URL=http://localhost:11434/v1/chat/completions
307
+ function cliCreativityEngine() {
308
+ const creativityDir = path.join(MIO_HOME, 'creativity')
309
+ return new CreativityEngine(creativityDir, chatJson)
310
+ }
311
+
312
+ // Observed-agent telemetry (agents.jsonl + traces/memory/reuse cross-reference)
313
+ // shares one store with the MCP server, so CLI and MCP report identical agents.
314
+ function cliAgentStore() {
315
+ return createAgentStore({ dataDir: MIO_HOME, projectName })
316
+ }
317
+
318
+ // Task routing shares one store with mio.task.route. persistQuery stays false:
319
+ // the MCP call feeds the query log for auto-claim, but a terminal inspection
320
+ // must not write to queries.jsonl every time it runs.
321
+ function cliTaskStore() {
322
+ // Shares one query-log instance with nothing else here, but created from the
323
+ // same module the MCP server uses, so queries.jsonl has a single reader/writer
324
+ // implementation. cliMemoryStore deliberately does NOT get one: a terminal
325
+ // recall is never followed by a task_outcome, so recording those queries
326
+ // would only add noise.
327
+ return createTaskStore({
328
+ dataDir: MIO_HOME,
329
+ projectName,
330
+ memoryStore: cliMemoryStore(),
331
+ queryLog: createQueryLog({ dataDir: MIO_HOME }),
332
+ agentId: () => 'cli',
333
+ })
334
+ }
335
+
241
336
  function splitTagsOption(args, name) {
242
337
  const raw = optionValue(args, name)
243
338
  if (raw === undefined) return []
@@ -348,6 +443,13 @@ function digestCommand(args, useJson) {
348
443
  // Feed the actionable lines back into each active workspace context file
349
444
  // (AGENTS.md / CLAUDE.md MIO_CONTEXT block), so every agent passively
350
445
  // receives the digest's value on its next session.
446
+ //
447
+ // Targets are resolved per project, never broadcast. An earlier version
448
+ // wrote one global headline into Object.keys(state.contexts) -- every
449
+ // workspace ever observed -- so unrelated repos (ComfyUI, douyin_store)
450
+ // received the same line and their real history was pushed out of the
451
+ // six-slot block. Write only where the digest actually has data for that
452
+ // project, or where the caller explicitly asked with --cwd/--project.
351
453
  const stateFile = path.join(MIO_HOME, 'observe-state.json')
352
454
  let state = {}
353
455
  try {
@@ -355,27 +457,50 @@ function digestCommand(args, useJson) {
355
457
  } catch (_) {}
356
458
  state.contexts = state.contexts || {}
357
459
  const stamp = new Date().toLocaleString('zh-CN', { hour12: false })
358
- const rate =
359
- report.overview.tasks > 0
360
- ? Math.round(((report.overview.byOutcome.success || 0) / report.overview.tasks) * 100)
361
- : 0
362
- const headline =
363
- `digest(${days}d): 任务 ${report.overview.tasks} 条 成功率 ${rate}%, error ${report.overview.errorTraces}` +
364
- (report.suggestions.length > 0 ? ' | ' + report.suggestions[0] : '')
365
- const targetCwds = optionValue(flags, '--cwd')
366
- ? [optionValue(flags, '--cwd')]
367
- : Object.keys(state.contexts)
460
+
461
+ // Project name -> per-project digest line.
462
+ const projectLine = new Map()
463
+ for (const entry of report.projects || []) {
464
+ if (!entry || !entry.project) continue
465
+ const rate = entry.tasks > 0 ? Math.round(((entry.success || 0) / entry.tasks) * 100) : 0
466
+ projectLine.set(String(entry.project).toLowerCase(), `digest(${days}d): ${entry.project} ${entry.tasks} 任务 ${rate}% 成功`)
467
+ }
468
+
469
+ const explicitCwd = optionValue(flags, '--cwd')
470
+ const explicitProject = optionValue(flags, '--project')
471
+ let targetCwds = []
472
+ if (explicitCwd) {
473
+ targetCwds = [explicitCwd]
474
+ } else if (explicitProject) {
475
+ const wanted = String(explicitProject).toLowerCase()
476
+ targetCwds = Object.keys(state.contexts).filter(
477
+ (cwd) => path.basename(cwd).toLowerCase() === wanted,
478
+ )
479
+ } else {
480
+ // No explicit target: only write to workspaces this digest has data for.
481
+ // A workspace with no matching project gets nothing.
482
+ targetCwds = Object.keys(state.contexts).filter((cwd) =>
483
+ projectLine.has(path.basename(cwd).toLowerCase()),
484
+ )
485
+ }
486
+
368
487
  let written = 0
369
488
  for (const cwd of targetCwds) {
489
+ const line = projectLine.get(path.basename(cwd).toLowerCase())
490
+ if (!line) continue
370
491
  try {
371
- observer.updateProjectContext(state, cwd, stamp + ' | ' + headline, 'CLAUDE.md')
372
- observer.updateProjectContext(state, cwd, stamp + ' | ' + headline, 'AGENTS.md')
492
+ observer.updateProjectContext(state, cwd, stamp + ' | ' + line, 'CLAUDE.md')
493
+ observer.updateProjectContext(state, cwd, stamp + ' | ' + line, 'AGENTS.md')
373
494
  written += 1
374
495
  } catch (_) {}
375
496
  }
376
497
  fs.mkdirSync(MIO_HOME, { recursive: true })
377
498
  fs.writeFileSync(stateFile, JSON.stringify(state) + '\n', 'utf8')
378
- report.writeBack = { workspaces: written, headline }
499
+ report.writeBack = {
500
+ workspaces: written,
501
+ skipped: Object.keys(state.contexts).length - written,
502
+ projects: [...projectLine.values()],
503
+ }
379
504
  }
380
505
 
381
506
  // Persist the report so it becomes part of the data asset itself.
@@ -388,7 +513,10 @@ function digestCommand(args, useJson) {
388
513
  console.log(report.markdown)
389
514
  console.log(`Report saved: ${reportFile}`)
390
515
  if (report.writeBack) {
391
- console.log(`Write-back: ${report.writeBack.workspaces} workspace(s) <- "${report.writeBack.headline}"`)
516
+ console.log(
517
+ `Write-back: ${report.writeBack.workspaces} workspace(s), ${report.writeBack.skipped} skipped (no digest data)`,
518
+ )
519
+ for (const line of report.writeBack.projects) console.log(` - ${line}`)
392
520
  }
393
521
  }
394
522
 
@@ -451,102 +579,1763 @@ function rememberCommand(args, useJson) {
451
579
  console.log(`Recorded ${record.id} (kind=${record.kind}, project=${record.project || 'global'}, scope=${record.scope})`)
452
580
  }
453
581
 
454
- function readConfig() {
455
- const fallback = { version: 1, home: MIO_HOME, agents: {} }
456
- if (!fs.existsSync(CONFIG_FILE)) return fallback
457
- try {
458
- return { ...fallback, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
459
- } catch (_) {
460
- return fallback
582
+ // Every flag that consumes the next token. A positional directly after one of
583
+ // these is that flag's value, not an id.
584
+ const VALUE_FLAGS = new Set([
585
+ '--ids', '--reason', '--project', '--limit', '--scope', '--keep',
586
+ '--by', '--notes', '--source-agent', '--target-agent', '--experience-id', '--status',
587
+ ])
588
+
589
+ // Accept ids both as `--ids a,b,c` and as bare positionals
590
+ // (`mio memory archive mem_a mem_b`).
591
+ function collectIds(flags) {
592
+ const ids = []
593
+ for (let i = 0; i < flags.length; i += 1) {
594
+ const token = flags[i]
595
+ if (token.startsWith('--')) continue
596
+ const previous = flags[i - 1]
597
+ if (previous && VALUE_FLAGS.has(previous)) continue
598
+ // Ids never contain commas, so a comma-separated positional is unambiguous.
599
+ for (const id of token.split(',')) {
600
+ const trimmed = id.trim()
601
+ if (trimmed) ids.push(trimmed)
602
+ }
603
+ }
604
+ const raw = optionValue(flags, '--ids')
605
+ if (raw !== undefined) {
606
+ for (const id of raw.split(',')) {
607
+ const trimmed = id.trim()
608
+ if (trimmed) ids.push(trimmed)
609
+ }
461
610
  }
611
+ return [...new Set(ids)]
462
612
  }
463
613
 
464
- function writeConfig(config) {
465
- ensureHome()
466
- fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
614
+ function printMemoryAnalyze(result) {
615
+ const project = result.project || 'all'
616
+ console.log(
617
+ `Memory analyze: ${result.total} active, ${result.archived} archived (project=${project})`,
618
+ )
619
+ console.log(`layers: project=${result.layers.project} global=${result.layers.global}`)
620
+ const kinds = Object.entries(result.byKind)
621
+ if (kinds.length > 0) {
622
+ console.log(`by kind: ${kinds.map(([kind, count]) => `${kind}=${count}`).join(', ')}`)
623
+ }
624
+
625
+ if (result.duplicates.length > 0) {
626
+ console.log(`\nDuplicate groups: ${result.duplicates.length}`)
627
+ for (const group of result.duplicates) {
628
+ console.log(`- ${group.size} record(s): ${group.records.map((record) => record.id).join(', ')}`)
629
+ const preview = String(group.records[0].content || '')
630
+ if (preview) {
631
+ console.log(` ${preview.length > 80 ? `${preview.slice(0, 80)}...` : preview}`)
632
+ }
633
+ }
634
+ }
635
+
636
+ if (result.lowQuality.length > 0) {
637
+ console.log(`\nLow quality: ${result.issues.lowQuality} record(s)`)
638
+ for (const record of result.lowQuality) {
639
+ console.log(`- ${record.id} [${record.kind || 'note'}] ${record.issues.join(', ')}`)
640
+ }
641
+ }
642
+
643
+ if (result.issues.duplicatesSkipped) {
644
+ console.log('\nDuplicate scan skipped: too many active records to compare pairwise.')
645
+ }
646
+
647
+ if (result.suggestions.length > 0) {
648
+ console.log('')
649
+ for (const suggestion of result.suggestions) console.log(`suggestion: ${suggestion}`)
650
+ }
467
651
  }
468
652
 
469
- function jsonOrText(payload, useJson) {
470
- if (useJson) {
471
- console.log(JSON.stringify(payload, null, 2))
653
+ // Merging divergent content is refused by default; when that happens the
654
+ // caller needs to see *what* differs before deciding, so print the candidates.
655
+ function printMemoryMerge(result) {
656
+ if (!result.merged) {
657
+ console.log(`No merge applied (project=${result.project || 'current'}): ${result.reason}`)
658
+ if (result.divergent.length > 0) {
659
+ console.log('\nDivergent content — inspect before choosing a survivor:')
660
+ for (const record of result.divergent) {
661
+ console.log(`- ${record.id} (${record.contentLength} chars, ${record.timestamp || 'no timestamp'})`)
662
+ console.log(` ${record.content.split('\n')[0].slice(0, 110)}`)
663
+ }
664
+ }
665
+ if (result.notFound.length > 0) console.log(`\nnot found: ${result.notFound.join(', ')}`)
666
+ if (result.outOfScope && result.outOfScope.length > 0) {
667
+ console.log(`\nbelongs to another project: ${result.outOfScope.join(', ')}`)
668
+ }
669
+ if (result.hint) console.log(`\n${result.hint}`)
670
+ process.exitCode = 1
472
671
  return
473
672
  }
474
- return payload
673
+
674
+ // A merge that archived nothing is a no-op: every requested id was already
675
+ // archived (i.e. these were merged before). Report it as failure the same way
676
+ // archive does, so scripts do not read success out of an idempotent re-run.
677
+ if (result.archivedCount === 0) {
678
+ console.log(`No merge applied (project=${result.project || 'current'}): nothing left to merge`)
679
+ if (result.skipped.length > 0) {
680
+ console.log(`already archived (previously merged): ${result.skipped.join(', ')}`)
681
+ console.log(`superseded by: ${result.survivor.id}`)
682
+ }
683
+ if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
684
+ process.exitCode = 1
685
+ return
686
+ }
687
+
688
+ console.log(
689
+ `Merged ${result.archivedCount} record(s) into ${result.survivor.id} (project=${result.project || 'current'})`,
690
+ )
691
+ console.log(`survivor: ${result.survivor.contentLength} chars, ${result.survivor.timestamp || 'no timestamp'}`)
692
+ if (result.divergentContent) console.log('note: content differed across members; the survivor body was chosen explicitly')
693
+ console.log(`supersedes: ${result.survivor.supersedes.join(', ')}`)
694
+ if (result.skipped.length > 0) console.log(`already archived: ${result.skipped.join(', ')}`)
695
+ if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
696
+ console.log(`Undo with: mio memory restore --ids ${result.archived.join(',')}`)
475
697
  }
476
698
 
477
- function initCommand() {
478
- ensureHome()
479
- const config = readConfig()
480
- if (!config.createdAt) config.createdAt = new Date().toISOString()
481
- writeConfig(config)
482
- console.log(`MIO_HOME ready: ${MIO_HOME}`)
699
+ function memoryUsage() {
700
+ console.log(`Usage:
701
+ mio memory analyze Report duplicates, low-quality records and kind histogram
702
+ mio memory archive --ids a,b Archive records (soft delete; hidden from recall/analyze)
703
+ mio memory restore --ids a,b Un-archive previously archived records
704
+ mio memory forget --ids a,b PERMANENTLY delete records (--yes required; writes an audit entry)
705
+ mio memory merge --ids a,b Merge duplicates: one survives, the rest are archived
706
+ mio memory migrate --ids a,b --scope global Move records between project and global layers
707
+
708
+ Options:
709
+ --ids a,b,c Memory record ids (also accepted as bare positionals)
710
+ --keep id Merge: which record's content survives (default: newest)
711
+ --allow-divergent Merge: permit merging records whose content differs (requires --keep)
712
+ --scope global|project Target layer for migrate
713
+ --project name Project filter (defaults to current directory name)
714
+ --reason text Optional note stored on the record
715
+ --limit n Max duplicate groups / low-quality rows to show (analyze, 1-20)
716
+ --yes Apply archive/merge (without it they only preview)
717
+ --json Machine-readable output
718
+ `)
483
719
  }
484
720
 
485
- function statusCommand(useJson) {
486
- const config = readConfig()
487
- const codexRestart = adapters.codex.restartWarningMessage()
488
- const opencodeRestart = adapters.opencode.restartWarningMessage()
489
- const installed = {
490
- codex: adapters.codex.isInstalled(),
491
- opencode: adapters.opencode.isInstalled(),
492
- workbuddy: adapters.workbuddy.isInstalled(),
493
- hermes: adapters.hermes.isInstalled(),
494
- claude: adapters.claude.isInstalled(),
721
+ function memoryCommand(args, useJson) {
722
+ const sub = args[1]
723
+ const flags = args.slice(2)
724
+
725
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
726
+ memoryUsage()
727
+ if (!sub) process.exitCode = 1
728
+ return
495
729
  }
496
- const payload = {
497
- version: config.version,
498
- home: MIO_HOME,
499
- serverScript: SERVER_SCRIPT,
500
- serverScriptExists: fs.existsSync(SERVER_SCRIPT),
501
- agents: config.agents || {},
502
- codexInstalled: installed.codex,
503
- opencodeInstalled: installed.opencode,
504
- workbuddyInstalled: installed.workbuddy,
505
- hermesInstalled: installed.hermes,
506
- claudeInstalled: installed.claude,
507
- codexNeedsRestart: Boolean(codexRestart),
508
- opencodeNeedsRestart: Boolean(opencodeRestart),
509
- codexRestartHint: codexRestart,
510
- opencodeRestartHint: opencodeRestart,
511
- hermesObserverReady: fs.existsSync(observer.hermesDbPath()),
512
- claudeObserverReady: fs.existsSync(observer.claudeProjectsDir()),
513
- opencodeObserverReady: fs.existsSync(observer.opencodeDbPath()),
514
- observerRunning: observer.isRunning(MIO_HOME),
515
- observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
730
+ if (!['analyze', 'archive', 'restore', 'forget', 'merge', 'migrate'].includes(sub)) {
731
+ console.error(`Unknown memory subcommand: ${sub}`)
732
+ memoryUsage()
733
+ process.exitCode = 1
734
+ return
516
735
  }
517
- if (useJson) {
518
- jsonOrText(payload, true)
736
+
737
+ const store = cliMemoryStore()
738
+
739
+ if (sub === 'analyze') {
740
+ let result
741
+ try {
742
+ result = store.analyzeMemory({
743
+ project: optionValue(flags, '--project'),
744
+ limit: parseNumberOption(flags, '--limit'),
745
+ })
746
+ } catch (error) {
747
+ console.error(error.message || error)
748
+ process.exitCode = 1
749
+ return
750
+ }
751
+ if (useJson) return jsonOrText(result, true)
752
+ return printMemoryAnalyze(result)
753
+ }
754
+
755
+ const ids = collectIds(flags)
756
+ if (ids.length === 0) {
757
+ console.error(`mio memory ${sub} requires --ids <id,id,...> (or bare ids)`)
758
+ process.exitCode = 1
519
759
  return
520
760
  }
521
- console.log(`MIO_HOME: ${MIO_HOME}`)
522
- console.log(`MCP Server: ${SERVER_SCRIPT}`)
523
- console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
524
- if (codexRestart) console.log(`Codex app: running with an older Mio config - fully restart Codex to load it`)
525
- console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
526
- if (opencodeRestart) console.log(`OpenCode Desktop: running with an older Mio config - restart it to load it`)
527
- console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
528
- console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
529
- console.log(`Hermes MCP: ${installed.hermes ? 'installed' : 'not installed'}`)
530
- console.log(`Hermes observer: ${fs.existsSync(observer.hermesDbPath()) ? 'ready' : 'no database'}`)
531
- console.log(`Claude Code MCP: ${installed.claude ? 'installed' : 'not installed'}`)
532
- console.log(`Claude Code observer: ${fs.existsSync(observer.claudeProjectsDir()) ? 'ready' : 'no transcripts'}`)
533
- console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
761
+ if (sub === 'merge' && ids.length < 2) {
762
+ console.error('mio memory merge requires at least 2 ids (nothing to merge otherwise)')
763
+ process.exitCode = 1
764
+ return
765
+ }
766
+
767
+ if ((sub === 'archive' || sub === 'merge' || sub === 'forget') && !flagPresent(flags, '--yes')) {
768
+ // Archiving hides records from recall, merging additionally rewrites the
769
+ // survivor's supersedes list, and forgetting deletes outright. Preview first
770
+ // so a mistyped id cannot silently destroy data.
771
+ const previewProject = optionValue(flags, '--project') || projectName() || 'current'
772
+ if (useJson) {
773
+ jsonOrText(
774
+ {
775
+ preview: true,
776
+ applied: false,
777
+ project: previewProject,
778
+ ids,
779
+ keep: optionValue(flags, '--keep') || null,
780
+ reversible: sub !== 'forget',
781
+ hint:
782
+ sub === 'forget'
783
+ ? 'PERMANENT: re-run with --yes to delete. Consider mio memory archive instead.'
784
+ : 'Re-run with --yes to apply.',
785
+ },
786
+ true,
787
+ )
788
+ } else if (sub === 'forget') {
789
+ // Show *what* is about to disappear, not just the ids: this is the one
790
+ // memory operation that cannot be undone.
791
+ const all = readJsonl(cliMemoryStore().memoryPath)
792
+ const idSet = new Set(ids)
793
+ const matched = all.filter((r) => idSet.has(r.id))
794
+ console.log(`Forget preview: ${ids.length} id(s) (project=${previewProject})`)
795
+ for (const record of matched) {
796
+ console.log(` ${record.id} ${String(record.content || '').replace(/\s+/g, ' ').slice(0, 80)}`)
797
+ }
798
+ for (const id of ids) {
799
+ if (!matched.some((r) => r.id === id)) console.log(` ${id} (not found)`)
800
+ }
801
+ console.log('')
802
+ console.log('WARNING: this permanently deletes the record(s) and cannot be undone.')
803
+ console.log('An audit entry (excerpt only) is written to memory-forget-audit.jsonl.')
804
+ console.log(`Reversible alternative: mio memory archive --ids ${ids.join(',')} --yes`)
805
+ console.log('Re-run with --yes to delete permanently.')
806
+ } else {
807
+ // Archive keeps its historical "Archive preview" wording; merge is newer
808
+ // and uses a lowercase verb to read naturally mid-sentence.
809
+ const label = sub === 'merge' ? 'merge preview' : 'Archive preview'
810
+ console.log(`${label}: ${ids.length} id(s) (project=${previewProject})`)
811
+ console.log(` ${ids.join(', ')}`)
812
+ if (sub === 'merge') {
813
+ const keep = optionValue(flags, '--keep')
814
+ console.log(` survivor: ${keep || 'newest by timestamp (default)'}`)
815
+ }
816
+ console.log(`Re-run with --yes to apply. Undo with: mio memory restore --ids <ids>`)
817
+ }
818
+ process.exitCode = 1
819
+ return
820
+ }
821
+
822
+ let result
823
+ try {
824
+ if (sub === 'archive' || sub === 'restore') {
825
+ result = store.archiveMemory({
826
+ ids,
827
+ project: optionValue(flags, '--project'),
828
+ restore: sub === 'restore',
829
+ reason: optionValue(flags, '--reason'),
830
+ })
831
+ } else if (sub === 'merge') {
832
+ result = store.mergeMemory({
833
+ ids,
834
+ project: optionValue(flags, '--project'),
835
+ keep: optionValue(flags, '--keep'),
836
+ allowDivergent: flagPresent(flags, '--allow-divergent'),
837
+ reason: optionValue(flags, '--reason'),
838
+ })
839
+ } else if (sub === 'forget') {
840
+ result = store.forgetMemory({
841
+ ids,
842
+ project: optionValue(flags, '--project'),
843
+ reason: optionValue(flags, '--reason'),
844
+ by: 'cli',
845
+ })
846
+ } else {
847
+ result = store.migrateMemory({
848
+ ids,
849
+ scope: optionValue(flags, '--scope'),
850
+ project: optionValue(flags, '--project'),
851
+ reason: optionValue(flags, '--reason'),
852
+ })
853
+ }
854
+ } catch (error) {
855
+ console.error(error.message || error)
856
+ process.exitCode = 1
857
+ return
858
+ }
859
+
860
+ if (useJson) return jsonOrText(result, true)
861
+
862
+ if (sub === 'merge') return printMemoryMerge(result)
863
+
864
+ if (sub === 'forget') {
865
+ console.log(`Forgot ${result.forgottenCount} record(s) permanently (project=${result.project || 'current'})`)
866
+ if (result.forgotten.length > 0) console.log(`deleted: ${result.forgotten.join(', ')}`)
867
+ if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
868
+ console.log(`audit: ${result.auditPath}`)
869
+ // Everything missing is a typo; do not let a no-op read as success.
870
+ if (result.forgottenCount === 0) {
871
+ process.exitCode = 1
872
+ return
873
+ }
874
+ // No undo is possible -- say so, and point at what should have been used.
875
+ console.log('This cannot be undone. (Next time consider `mio memory archive`, which is reversible.)')
876
+ return
877
+ }
878
+
879
+ if (sub === 'migrate') {
880
+ const target = result.scope === 'global' ? 'global' : `project=${result.project || 'current'}`
881
+ console.log(`Migrated ${result.updatedCount} record(s) to ${target}`)
882
+ if (result.updated.length > 0) console.log(`updated: ${result.updated.join(', ')}`)
883
+ if (result.unchanged.length > 0) console.log(`already ${result.scope}: ${result.unchanged.join(', ')}`)
884
+ if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
885
+ // Every requested id missing is almost certainly a typo; signal it so
886
+ // scripts do not treat a no-op as success.
887
+ if (result.updatedCount === 0 && result.unchanged.length === 0) {
888
+ process.exitCode = 1
889
+ return
890
+ }
891
+ if (result.updated.length > 0) {
892
+ const back = result.scope === 'global' ? 'project' : 'global'
893
+ console.log(`Undo with: mio memory migrate --ids ${result.updated.join(',')} --scope ${back}`)
894
+ }
895
+ return
896
+ }
897
+
898
+ const verb = sub === 'restore' ? 'Restored' : 'Archived'
899
+ console.log(`${verb} ${result.archivedCount} record(s) (project=${result.project || 'current'})`)
900
+ if (result.archived.length > 0) console.log(`${sub}d: ${result.archived.join(', ')}`)
901
+ if (result.skipped.length > 0) console.log(`already ${sub}d: ${result.skipped.join(', ')}`)
902
+ if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
903
+ if (result.archivedCount === 0) {
904
+ // Nothing changed: either the ids do not exist or they were already in the
905
+ // target state. Either way the caller's intent was not fulfilled, so say so
906
+ // instead of silently exiting 0.
907
+ console.error(`No records ${sub}d. Check the ids, or run: mio memory analyze`)
908
+ process.exitCode = 1
909
+ return
910
+ }
911
+ if (sub === 'archive' && result.archived.length > 0) {
912
+ console.log(`Undo with: mio memory restore --ids ${result.archived.join(',')}`)
913
+ }
534
914
  }
535
915
 
536
- function agentsCommand(useJson) {
537
- const config = readConfig()
538
- const agents = config.agents || {}
539
- if (useJson) {
540
- jsonOrText(agents, true)
916
+ // The observer auto-claims experience reuse, but an unconfirmed auto_claim
917
+ // never counts as `verified` and so never feeds memory ranking or task
918
+ // routing. In practice most claims sat unconfirmed forever because confirming
919
+ // was only possible through the MCP server.
920
+ function cliExperienceStore() {
921
+ return createExperienceStore({
922
+ dataDir: MIO_HOME,
923
+ projectName,
924
+ })
925
+ }
926
+
927
+ function experienceUsage() {
928
+ console.log(`Usage:
929
+ mio experience list List reuse records (--status/--target-agent/--project/--limit)
930
+ mio experience confirm --ids a,b Confirm auto-claimed reuse (bulk supported)
931
+ mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse manually
932
+
933
+ Options:
934
+ --ids a,b,c Record ids (also accepted as bare positionals; confirm only)
935
+ --status pending|confirmed|verified|auto_claim|agent_report|all Filter for list (default all)
936
+ --target-agent name Only records reused by this agent
937
+ --experience-id id The memory/experience that was reused
938
+ --source-agent name Agent the experience came from
939
+ --reuse --behavior-changed --outcome-improved Booleans describing the reuse
940
+ --outcome-improved Confirm: accept the claim (--no-improved rejects it)
941
+ --by name Who is confirming (default: cli)
942
+ --notes text Optional note
943
+ --force Re-confirm records that are already confirmed
944
+ --project name Project filter (defaults to current directory name)
945
+ --json Machine-readable output
946
+ `)
947
+ }
948
+
949
+ function printExperienceList(result) {
950
+ console.log(
951
+ `Experience reuse: ${result.count} shown of ${result.total} (project=${result.project || 'all'}, status=${result.status})`,
952
+ )
953
+ if (result.count === 0) {
954
+ console.log('No reuse records match.')
955
+ return
956
+ }
957
+ result.records.forEach((record, index) => {
958
+ const state = record.confirmed ? 'confirmed' : 'pending'
959
+ const badge = record.reuse && record.behaviorChanged && record.outcomeImproved ? 'verified' : state
960
+ const arrow = `${record.sourceAgent || '?'} -> ${record.targetAgent || '?'}`
961
+ const flags = [
962
+ record.reuse ? 'reused' : null,
963
+ record.behaviorChanged ? 'changed' : null,
964
+ record.outcomeImproved ? 'improved' : null,
965
+ ]
966
+ .filter(Boolean)
967
+ .join(',')
968
+ console.log(`${index + 1}. [${badge}] ${record.experienceId} ${arrow}`)
969
+ console.log(
970
+ ` ${flags || 'no effect flags'} | ${(record.timestamp || '').slice(0, 19)} | ${record.id}`,
971
+ )
972
+ if (record.notes) console.log(` note: ${record.notes}`)
973
+ })
974
+ // Only auto_claims are confirmable — experience.confirm rejects anything else.
975
+ const pending = result.records
976
+ .filter((record) => record.source === 'auto_claim' && !record.confirmed)
977
+ .map((record) => record.id)
978
+ if (pending.length > 0) {
979
+ console.log('')
980
+ console.log(
981
+ `${pending.length} unconfirmed auto-claim(s) shown. Unconfirmed auto-claims never count as verified, so they do not affect ranking or routing.`,
982
+ )
983
+ console.log(`Confirm with: mio experience confirm --ids ${pending.join(',')}`)
984
+ }
985
+ }
986
+
987
+ function experienceCommand(args, useJson) {
988
+ const sub = args[1]
989
+ const flags = args.slice(2)
990
+
991
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
992
+ experienceUsage()
993
+ if (!sub) process.exitCode = 1
994
+ return
995
+ }
996
+ if (!['list', 'confirm', 'reuse'].includes(sub)) {
997
+ console.error(`Unknown experience subcommand: ${sub}`)
998
+ experienceUsage()
999
+ process.exitCode = 1
1000
+ return
1001
+ }
1002
+
1003
+ const store = cliExperienceStore()
1004
+
1005
+ if (sub === 'list') {
1006
+ let result
1007
+ try {
1008
+ result = store.listReuse({
1009
+ status: optionValue(flags, '--status'),
1010
+ targetAgent: optionValue(flags, '--target-agent'),
1011
+ project: optionValue(flags, '--project'),
1012
+ limit: parseNumberOption(flags, '--limit'),
1013
+ })
1014
+ } catch (error) {
1015
+ console.error(error.message || error)
1016
+ process.exitCode = 1
1017
+ return
1018
+ }
1019
+ if (useJson) return jsonOrText(result, true)
1020
+ return printExperienceList(result)
1021
+ }
1022
+
1023
+ let result
1024
+ try {
1025
+ if (sub === 'confirm') {
1026
+ const ids = collectIds(flags)
1027
+ if (ids.length === 0) {
1028
+ console.error('mio experience confirm requires --ids <id,id,...> (or bare ids)')
1029
+ process.exitCode = 1
1030
+ return
1031
+ }
1032
+ // Absent = keep whatever the auto-claim recorded; --no-improved rejects it.
1033
+ const improved = flagPresent(flags, '--no-improved')
1034
+ ? false
1035
+ : flagPresent(flags, '--outcome-improved')
1036
+ ? true
1037
+ : undefined
1038
+ result = store.confirmReuse({
1039
+ ids,
1040
+ outcomeImproved: improved,
1041
+ confirmedBy: optionValue(flags, '--by') || 'cli',
1042
+ notes: optionValue(flags, '--notes'),
1043
+ force: flagPresent(flags, '--force'),
1044
+ })
1045
+ } else {
1046
+ result = store.recordReuse({
1047
+ sourceAgent: optionValue(flags, '--source-agent'),
1048
+ targetAgent: optionValue(flags, '--target-agent'),
1049
+ experienceId: optionValue(flags, '--experience-id'),
1050
+ reuse: flagPresent(flags, '--reuse'),
1051
+ behaviorChanged: flagPresent(flags, '--behavior-changed'),
1052
+ outcomeImproved: flagPresent(flags, '--outcome-improved'),
1053
+ project: optionValue(flags, '--project'),
1054
+ notes: optionValue(flags, '--notes'),
1055
+ })
1056
+ }
1057
+ } catch (error) {
1058
+ console.error(error.message || error)
1059
+ process.exitCode = 1
541
1060
  return
542
1061
  }
543
- const names = Object.keys(agents)
544
- if (names.length === 0) {
545
- console.log('No agents installed.')
1062
+
1063
+ if (useJson) return jsonOrText(result, true)
1064
+
1065
+ if (sub === 'reuse') {
1066
+ console.log(
1067
+ `Recorded reuse ${result.evidence.id}: ${result.evidence.sourceAgent} -> ${result.evidence.targetAgent} (${result.evidence.experienceId})`,
1068
+ )
546
1069
  return
547
1070
  }
548
- for (const name of names) {
549
- console.log(`${name}\t${agents[name].installedAt || ''}`)
1071
+
1072
+ console.log(`Confirmed ${result.confirmedCount} reuse record(s)`)
1073
+ if (result.confirmedIds.length > 0) console.log(`confirmed: ${result.confirmedIds.join(', ')}`)
1074
+ if (result.rejected.length > 0) {
1075
+ console.log(`skipped (not auto_claim): ${result.rejected.join(', ')}`)
1076
+ }
1077
+ if (result.alreadyConfirmed.length > 0) {
1078
+ console.log(`already confirmed: ${result.alreadyConfirmed.join(', ')} (use --force to redo)`)
1079
+ }
1080
+ if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
1081
+ if (result.confirmedCount === 0) {
1082
+ console.error('Nothing confirmed. Check the ids, or run: mio experience list --status pending')
1083
+ process.exitCode = 1
1084
+ }
1085
+ }
1086
+
1087
+ // mio.policy.check has always been able to answer "has this action failed
1088
+ // before?", but only from inside an MCP session -- so in practice nobody asked
1089
+ // before running the command. This gives it a terminal entry point.
1090
+ function policyUsage() {
1091
+ console.log(`Usage:
1092
+ mio policy check "<action>" Historical risk for an action, from trace + memory evidence
1093
+
1094
+ Options:
1095
+ --project name Project filter (defaults to current directory name)
1096
+ --json Machine-readable output (same shape as mio.policy.check)
1097
+
1098
+ Examples:
1099
+ mio policy check "npm publish"
1100
+ mio policy check "git reset --hard" --project akemi-mio
1101
+
1102
+ Reading the result:
1103
+ unknown no history for this action; treat as normal risk
1104
+ low history exists and is mostly clean
1105
+ moderate add verification before proceeding
1106
+ low/high driven by the failure share of matching traces`)
1107
+ }
1108
+
1109
+ function printPolicyCheck(result) {
1110
+ const level = result.riskLevel.toUpperCase()
1111
+ console.log(`Policy check: "${result.action}" (project=${result.project || 'all'})`)
1112
+ console.log(`risk: ${level}${result.risk === null ? '' : ` (${result.risk})`}`)
1113
+ if (result.total === 0) {
1114
+ console.log('evidence: no matching traces')
1115
+ } else {
1116
+ console.log(`evidence: ${result.total} matching trace(s), ${result.failures} failure(s)`)
1117
+ const outcomes = Object.entries(result.outcomeCounts)
1118
+ if (outcomes.length > 0) {
1119
+ console.log(`outcomes: ${outcomes.map(([k, v]) => `${k}=${v}`).join(', ')}`)
1120
+ }
1121
+ }
1122
+
1123
+ // A risk level derived from tokens that match most of the corpus looks
1124
+ // authoritative but means nothing. Say so instead of letting it stand.
1125
+ const diag = result.diagnostics
1126
+ if (diag && diag.lowSignal) {
1127
+ console.log('')
1128
+ console.log(
1129
+ `WARNING: low-signal match. Every token in this action (${diag.genericTokens.join(', ')}) ` +
1130
+ 'appears in most traces regardless of subject, so the level above is not meaningful.',
1131
+ )
1132
+ console.log('Try a more specific action, e.g. mio policy check "npm publish".')
1133
+ } else if (diag && diag.lowSample) {
1134
+ console.log('')
1135
+ console.log(
1136
+ `NOTE: only ${diag.sampleSize} matching trace(s); treat the level above as a weak signal.`,
1137
+ )
1138
+ }
1139
+
1140
+ console.log(`\n${result.suggestion}`)
1141
+
1142
+ if (result.failureExamples.length > 0) {
1143
+ console.log('\nRecent failures:')
1144
+ for (const example of result.failureExamples) {
1145
+ const when = example.timestamp ? example.timestamp.slice(0, 19).replace('T', ' ') : 'unknown time'
1146
+ console.log(`- [${example.outcome || 'failure'}] ${when}`)
1147
+ console.log(` ${example.summary}`)
1148
+ if (example.trace_id) console.log(` trace: ${example.trace_id}`)
1149
+ }
1150
+ }
1151
+
1152
+ if (result.guidance.level !== 'none') {
1153
+ console.log(`\nGuidance (${result.guidance.level}): ${result.guidance.rationale}`)
1154
+ if (result.guidance.avoid.length > 0) {
1155
+ console.log('avoid repeating:')
1156
+ for (const item of result.guidance.avoid) console.log(`- ${item}`)
1157
+ }
1158
+ for (const step of result.guidance.verificationSteps) console.log(`- ${step}`)
1159
+ }
1160
+
1161
+ if (result.related_memories.length > 0) {
1162
+ console.log('\nRelated memory:')
1163
+ for (const record of result.related_memories) {
1164
+ const badge = record.evidence ? ` [verified x${record.evidence.reuseCount}]` : ''
1165
+ console.log(`- ${record.id}${badge} ${String(record.content).replace(/\s+/g, ' ').slice(0, 110)}`)
1166
+ }
1167
+ }
1168
+ }
1169
+
1170
+ function policyCommand(args, useJson) {
1171
+ const sub = args[1]
1172
+
1173
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
1174
+ policyUsage()
1175
+ if (!sub) process.exitCode = 1
1176
+ return
1177
+ }
1178
+ if (sub !== 'check') {
1179
+ console.error(`Unknown policy subcommand: ${sub}`)
1180
+ policyUsage()
1181
+ process.exitCode = 1
1182
+ return
1183
+ }
1184
+
1185
+ const flags = args.slice(2)
1186
+ // The action is a positional, and may be quoted as one argument or passed as
1187
+ // several words (`mio policy check npm publish`). Only flags this command
1188
+ // actually understands are treated as options -- anything else, including
1189
+ // `--hard` in `git reset --hard`, belongs to the action being described.
1190
+ const POLICY_KNOWN_FLAGS = new Set(['--project', '--json'])
1191
+ const actionParts = []
1192
+ for (let i = 0; i < flags.length; i += 1) {
1193
+ const token = flags[i]
1194
+ if (POLICY_KNOWN_FLAGS.has(token)) continue
1195
+ if (i > 0 && POLICY_KNOWN_FLAGS.has(flags[i - 1])) continue
1196
+ actionParts.push(token)
1197
+ }
1198
+ const action = actionParts.join(' ').trim()
1199
+
1200
+ if (!action) {
1201
+ console.error('mio policy check requires an action, e.g. mio policy check "npm publish"')
1202
+ process.exitCode = 1
1203
+ return
1204
+ }
1205
+
1206
+ let result
1207
+ try {
1208
+ result = cliPolicyStore().policyCheck({
1209
+ action,
1210
+ project: optionValue(flags, '--project'),
1211
+ })
1212
+ } catch (error) {
1213
+ console.error(error.message || error)
1214
+ process.exitCode = 1
1215
+ return
1216
+ }
1217
+
1218
+ if (useJson) return jsonOrText(result, true)
1219
+ printPolicyCheck(result)
1220
+ }
1221
+
1222
+ // ═════════════════════════════════════════════════════════════
1223
+ // mio insight — terminal entry point for mio.insight.*
1224
+ //
1225
+ // The insight engine (self-observation) shipped MCP-only: an agent could read
1226
+ // its own insights, but there was no way to see them from a terminal. These
1227
+ // commands delegate to the same shared store as the MCP tools, so the two
1228
+ // cannot disagree.
1229
+ //
1230
+ // `generate` is deliberately not exposed. It needs an LLM and the CLI has no
1231
+ // LLM wiring; the same call was made for creativity generate/ferment.
1232
+ // ═════════════════════════════════════════════════════════════
1233
+ function cliInsightStore() {
1234
+ return createInsightStore({ dataDir: MIO_HOME })
1235
+ }
1236
+
1237
+ function insightUsage() {
1238
+ console.log(`Usage:
1239
+ mio insight status Insight counts: total, reported, unreported, high-value
1240
+ mio insight list List insights
1241
+ mio insight mark-reported --ids a,b Mark insights as reported (acknowledged)
1242
+
1243
+ Options:
1244
+ --unreported Only unreported insights (list)
1245
+ --min-score N Minimum score (list)
1246
+ --detector name Filter by detector (list)
1247
+ --limit N Max insights (list)
1248
+ --ids a,b Comma-separated insight ids (mark-reported)
1249
+ --json Machine-readable output (same shape as mio.insight.*)
1250
+
1251
+ The insight engine is an optional package (@akemi-mio/insight). If it is not
1252
+ installed these commands say so, rather than reporting a misleading empty list.`)
1253
+ }
1254
+
1255
+ function printInsightStatus(result) {
1256
+ console.log('Insight engine')
1257
+ console.log(` total: ${result.total}`)
1258
+ console.log(` reported: ${result.reported}`)
1259
+ console.log(` unreported: ${result.unreported}`)
1260
+ console.log(` high-value: ${result.highValue}`)
1261
+ if (result.unreported > 0) {
1262
+ console.log('\nUnreported insights are waiting: mio insight list --unreported')
1263
+ }
1264
+ }
1265
+
1266
+ function printInsightList(insights) {
1267
+ if (insights.length === 0) {
1268
+ console.log('No insights match.')
1269
+ return
1270
+ }
1271
+ for (const item of insights) {
1272
+ const score = typeof item.score === 'number' ? item.score.toFixed(2) : 'n/a'
1273
+ const state = item.reported ? 'reported' : 'unreported'
1274
+ const detector = item.detector ? `, ${item.detector}` : ''
1275
+ console.log(`- [${score}] ${item.id || '(no id)'} (${state}${detector})`)
1276
+ const text = String(item.content || item.summary || item.title || '')
1277
+ if (text) console.log(` ${text.replace(/\s+/g, ' ').slice(0, 140)}`)
1278
+ }
1279
+ console.log(`\n${insights.length} insight(s)`)
1280
+ }
1281
+
1282
+ function insightCommand(args, useJson) {
1283
+ const sub = args[1]
1284
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
1285
+ insightUsage()
1286
+ if (!sub) process.exitCode = 1
1287
+ return
1288
+ }
1289
+ if (!['status', 'list', 'mark-reported'].includes(sub)) {
1290
+ console.error(`Unknown insight subcommand: ${sub}`)
1291
+ insightUsage()
1292
+ process.exitCode = 1
1293
+ return
1294
+ }
1295
+
1296
+ const flags = args.slice(2)
1297
+ const store = cliInsightStore()
1298
+ try {
1299
+ if (sub === 'status') {
1300
+ const result = store.status()
1301
+ if (useJson) return jsonOrText(result, true)
1302
+ return printInsightStatus(result)
1303
+ }
1304
+ if (sub === 'list') {
1305
+ const result = store.list({
1306
+ unreported: flagPresent(flags, '--unreported'),
1307
+ minScore: parseNumberOption(flags, '--min-score'),
1308
+ detector: optionValue(flags, '--detector'),
1309
+ limit: parseNumberOption(flags, '--limit'),
1310
+ })
1311
+ if (useJson) return jsonOrText(result, true)
1312
+ return printInsightList(result)
1313
+ }
1314
+ const ids = splitTagsOption(flags, '--ids')
1315
+ if (ids.length === 0) {
1316
+ console.error('mio insight mark-reported requires --ids a,b')
1317
+ process.exitCode = 1
1318
+ return
1319
+ }
1320
+ const result = store.markReported({ ids })
1321
+ if (useJson) return jsonOrText(result, true)
1322
+ console.log(`Marked ${result.marked} insight(s) as reported.`)
1323
+ } catch (error) {
1324
+ console.error(error.message || error)
1325
+ process.exitCode = 1
1326
+ }
1327
+ }
1328
+
1329
+ // ═════════════════════════════════════════════════════════════
1330
+ // mio observer — terminal entry point for the observer research pipeline
1331
+ // (mio.observer.*). Not to be confused with `mio observe`, which is the
1332
+ // WorkBuddy transcript daemon.
1333
+ //
1334
+ // Only the read-only views are exposed. collect hits the network and ferment
1335
+ // drives the engine; both belong to the daemon, not an interactive shell (same
1336
+ // reasoning as skipping creativity generate from the CLI).
1337
+ //
1338
+ // Base directory defaults to <cwd>/.local/observer -- the MCP server's default
1339
+ // -- because the pipeline is project-local by design. --base-dir overrides it.
1340
+ // ═════════════════════════════════════════════════════════════
1341
+ function cliObserverStore() {
1342
+ return createObserverStore({})
1343
+ }
1344
+
1345
+ function observerUsage() {
1346
+ console.log(`Usage:
1347
+ mio observer status Pipeline stage counts (observations/trends/topics/...)
1348
+ mio observer world-model Entities, events, trends and narratives
1349
+ mio observer trends Recent trend reports
1350
+ mio observer research Research reports
1351
+ mio observer insights Observer-generated insight articles
1352
+ mio observer essays Published essays
1353
+ mio observer dag Daily summaries for the last N days
1354
+ mio observer ingest Record a trace event (tool_call/error/retry/task_outcome)
1355
+ --trace-id <id> --event-type <type> [--payload '<json>'] [--outcome]
1356
+ [--agent] [--host] [--project] (writes immediately, like mio remember)
1357
+ mio observer subscribe Subscribe to events: --event-types a,b [--topic T] [--ttl-days N]
1358
+ (--yes required to apply; previews by default)
1359
+ mio observer digest New events since the last digest (--limit/--project/--event-types)
1360
+ NOTE: advances the cursor, so a second run returns only newer events
1361
+
1362
+ Options:
1363
+ --base-dir DIR Observer data directory (default: <cwd>/.local/observer)
1364
+ --date YYYY-MM-DD A single trend report (trends)
1365
+ --type name Essay type (default: published)
1366
+ --days N Days of history (dag, default 7)
1367
+ --limit N Max items
1368
+ --json Machine-readable output (same shape as mio.observer.*)`)
1369
+ }
1370
+
1371
+ // Pipeline items are free-form JSON written by different stages, so pull a
1372
+ // human label from whichever well-known field happens to be present.
1373
+ function observerItemLabel(item) {
1374
+ if (item === null || typeof item !== 'object') return String(item)
1375
+ for (const key of ['title', 'name', 'topic', 'headline', 'summary', 'text', 'id', 'date']) {
1376
+ const value = item[key]
1377
+ if (typeof value === 'string' && value.trim()) return value.trim().replace(/\s+/g, ' ').slice(0, 120)
1378
+ }
1379
+ return JSON.stringify(item).slice(0, 120)
1380
+ }
1381
+
1382
+ function printObserverList(kind, items) {
1383
+ if (items.length === 0) {
1384
+ console.log(`No ${kind} found. (Run the observer pipeline, or check --base-dir.)`)
1385
+ return
1386
+ }
1387
+ for (const item of items) console.log(`- ${observerItemLabel(item)}`)
1388
+ console.log(`\n${items.length} ${kind}`)
1389
+ }
1390
+
1391
+ function printObserverStatus(status) {
1392
+ console.log('Observer pipeline (file counts per stage)')
1393
+ let total = 0
1394
+ for (const [stage, count] of Object.entries(status)) {
1395
+ total += count
1396
+ console.log(` ${stage.padEnd(14)} ${count}`)
1397
+ }
1398
+ console.log(`\n ${'total'.padEnd(14)} ${total}`)
1399
+ if (total === 0) {
1400
+ console.log('\nNo pipeline data yet. Nothing has been collected into this directory.')
1401
+ }
1402
+ }
1403
+
1404
+ function printObserverWorldModel(model) {
1405
+ const sections = ['entities', 'events', 'trends', 'narratives']
1406
+ console.log('World model')
1407
+ for (const key of sections) {
1408
+ const value = model[key]
1409
+ const count = Array.isArray(value) ? value.length : Object.keys(value || {}).length
1410
+ console.log(` ${key.padEnd(12)} ${count}`)
1411
+ }
1412
+ if (sections.every((key) => (Array.isArray(model[key]) ? model[key].length : 0) === 0)) {
1413
+ console.log('\nWorld model is empty.')
1414
+ }
1415
+ }
1416
+
1417
+ function printObserverEssays(essays) {
1418
+ if (essays.length === 0) {
1419
+ console.log('No essays found.')
1420
+ return
1421
+ }
1422
+ for (const essay of essays) {
1423
+ console.log(`- ${essay.file} (${essay.type}, ${String(essay.created).slice(0, 10)})`)
1424
+ const firstLine = String(essay.content || '').split('\n').find((l) => l.trim()) || ''
1425
+ if (firstLine) console.log(` ${firstLine.replace(/\s+/g, ' ').slice(0, 120)}`)
1426
+ }
1427
+ console.log(`\n${essays.length} essay(s)`)
1428
+ }
1429
+
1430
+ function printObserverDag(result, days) {
1431
+ if (result.summaryCount === 0) {
1432
+ console.log(`No daily summaries found in the last ${days || 7} day(s).`)
1433
+ return
1434
+ }
1435
+ for (const day of result.summaries) {
1436
+ console.log(`- ${day.date}: ${day.summaries.length} summary(ies)`)
1437
+ }
1438
+ console.log(`\n${result.summaryCount} summary(ies) across ${result.summaries.length} day(s)`)
1439
+ }
1440
+
1441
+ function cliSubscriptionStore() {
1442
+ return createSubscriptionStore({
1443
+ dataDir: MIO_HOME,
1444
+ projectName,
1445
+ agentId: () => 'cli',
1446
+ })
1447
+ }
1448
+
1449
+ // Records an arbitrary trace event (tool_call / error / retry / task_outcome).
1450
+ // Unlike `mio task record-outcome`, which is specific to task outcomes, this is
1451
+ // the general-purpose half -- and the one AGENTS.md asks agents to call.
1452
+ //
1453
+ // It appends rather than modifies, so like `mio remember` it writes straight
1454
+ // away instead of previewing: there is nothing to destroy. Missing arguments are
1455
+ // still rejected before anything is written.
1456
+ function observerIngestCommand(args, useJson) {
1457
+ const flags = args.slice(2)
1458
+ const traceId = optionValue(flags, '--trace-id')
1459
+ const eventType = optionValue(flags, '--event-type')
1460
+ if (!traceId || !eventType) {
1461
+ console.error('mio observer ingest requires --trace-id <id> and --event-type <type>')
1462
+ console.error('Example: mio observer ingest --trace-id t1 --event-type tool_call --payload \'{"tool":"Bash"}\'')
1463
+ process.exitCode = 1
1464
+ return
1465
+ }
1466
+
1467
+ let payload = {}
1468
+ const rawPayload = optionValue(flags, '--payload')
1469
+ if (rawPayload) {
1470
+ try {
1471
+ payload = JSON.parse(rawPayload)
1472
+ } catch (error) {
1473
+ console.error(`--payload must be valid JSON: ${error.message}`)
1474
+ process.exitCode = 1
1475
+ return
1476
+ }
1477
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
1478
+ console.error('--payload must be a JSON object')
1479
+ process.exitCode = 1
1480
+ return
1481
+ }
1482
+ }
1483
+
1484
+ let result
1485
+ try {
1486
+ result = cliTaskStore().ingestObservation({
1487
+ trace_id: traceId,
1488
+ event_type: eventType,
1489
+ outcome: optionValue(flags, '--outcome'),
1490
+ payload,
1491
+ agent: optionValue(flags, '--agent'),
1492
+ host: optionValue(flags, '--host'),
1493
+ project: optionValue(flags, '--project'),
1494
+ })
1495
+ } catch (error) {
1496
+ console.error(error.message || error)
1497
+ process.exitCode = 1
1498
+ return
1499
+ }
1500
+
1501
+ if (useJson) return jsonOrText(result, true)
1502
+ console.log(`Ingested ${result.event.event_type} (trace=${result.event.trace_id})`)
1503
+ console.log(` agent=${result.event.agent} project=${result.event.project} host=${result.event.host}`)
1504
+ if (result.autoClaims && result.autoClaims.length > 0) {
1505
+ console.log(` auto-claims: ${result.autoClaims.length}`)
1506
+ }
1507
+ }
1508
+
1509
+ // Subscribe creates or renews a subscription, so it previews like every other
1510
+ // mutating command (`mio agents register`, `mio memory archive`): exit 1 and
1511
+ // nothing written without --yes.
1512
+ function observerSubscribeCommand(args, useJson) {
1513
+ const flags = args.slice(2)
1514
+ const project = optionValue(flags, '--project') || projectName()
1515
+ const eventTypes = splitTagsOption(flags, '--event-types')
1516
+ const topic = optionValue(flags, '--topic') || ''
1517
+ const ttlDays = optionValue(flags, '--ttl-days')
1518
+
1519
+ if (!flagPresent(flags, '--yes')) {
1520
+ const store = cliSubscriptionStore()
1521
+ const existing = readJsonl(store.subscriptionPath).find((subscription) => {
1522
+ return (
1523
+ subscription &&
1524
+ subscription.agent === 'cli' &&
1525
+ subscribeKey(subscription.agent, subscription.project, subscription.eventTypes || [], subscription.topic || '') ===
1526
+ subscribeKey('cli', project, eventTypes, topic)
1527
+ )
1528
+ })
1529
+ if (useJson) {
1530
+ jsonOrText(
1531
+ {
1532
+ preview: true,
1533
+ applied: false,
1534
+ project,
1535
+ eventTypes,
1536
+ topic: topic || null,
1537
+ ttlDays: ttlDays ? Number(ttlDays) : null,
1538
+ willCreate: !existing,
1539
+ hint: 'Re-run with --yes to apply.',
1540
+ },
1541
+ true,
1542
+ )
1543
+ } else {
1544
+ console.log(`Subscribe preview: project=${project}`)
1545
+ console.log(` event types: ${eventTypes.length > 0 ? eventTypes.join(', ') : '(all)'}`)
1546
+ if (topic) console.log(` topic: ${topic}`)
1547
+ console.log(` ${existing ? 'will renew the existing subscription' : 'will create a new subscription'}`)
1548
+ console.log('Re-run with --yes to apply.')
1549
+ }
1550
+ process.exitCode = 1
1551
+ return
1552
+ }
1553
+
1554
+ let result
1555
+ try {
1556
+ result = cliSubscriptionStore().subscribe({ project, eventTypes, topic, ttlDays })
1557
+ } catch (error) {
1558
+ console.error(error.message || error)
1559
+ process.exitCode = 1
1560
+ return
1561
+ }
1562
+ if (useJson) return jsonOrText(result, true)
1563
+ console.log(`Subscribed (project=${result.project})`)
1564
+ console.log(` id=${result.subscription.id} expires=${result.subscription.expiresAt}`)
1565
+ console.log(` event types: ${(result.subscription.eventTypes || []).join(', ') || '(all)'}`)
1566
+ }
1567
+
1568
+ // The digest is cursor-based: it returns only events newer than the last one it
1569
+ // delivered, and ADVANCES that cursor. Running it twice therefore gives different
1570
+ // results -- so it cannot be previewed, and the output says so rather than
1571
+ // leaving that surprise for the user to discover.
1572
+ function observerDigestCommand(args, useJson) {
1573
+ const flags = args.slice(2)
1574
+ let result
1575
+ try {
1576
+ result = cliSubscriptionStore().digest({
1577
+ project: optionValue(flags, '--project'),
1578
+ limit: optionValue(flags, '--limit'),
1579
+ eventTypes: splitTagsOption(flags, '--event-types'),
1580
+ })
1581
+ } catch (error) {
1582
+ console.error(error.message || error)
1583
+ process.exitCode = 1
1584
+ return
1585
+ }
1586
+ if (useJson) return jsonOrText(result, true)
1587
+ console.log(`Observer digest: ${result.count} event(s) (project=${result.project}, agent=${result.agent})`)
1588
+ console.log(` subscriptions: ${result.subscriptionCount} active, ${result.matchedSubscriptions.length} matched`)
1589
+ for (const event of result.events) {
1590
+ console.log(`- ${event.event_type}${event.outcome ? ` (${event.outcome})` : ''} trace=${event.trace_id || '-'}`)
1591
+ }
1592
+ if (result.count === 0) {
1593
+ console.log('Nothing new since the last digest.')
1594
+ }
1595
+ console.log('Note: this advances the cursor; the next digest returns only newer events.')
1596
+ }
1597
+
1598
+ function observerCommand(args, useJson) {
1599
+ const sub = args[1]
1600
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
1601
+ observerUsage()
1602
+ if (!sub) process.exitCode = 1
1603
+ return
1604
+ }
1605
+ if (sub === 'ingest') return observerIngestCommand(args, useJson)
1606
+ if (sub === 'subscribe') return observerSubscribeCommand(args, useJson)
1607
+ if (sub === 'digest') return observerDigestCommand(args, useJson)
1608
+
1609
+ if (!['status', 'world-model', 'trends', 'research', 'insights', 'essays', 'dag'].includes(sub)) {
1610
+ console.error(`Unknown observer subcommand: ${sub}`)
1611
+ observerUsage()
1612
+ process.exitCode = 1
1613
+ return
1614
+ }
1615
+
1616
+ const flags = args.slice(2)
1617
+ const store = cliObserverStore()
1618
+ const base = { baseDir: optionValue(flags, '--base-dir') }
1619
+ try {
1620
+ if (sub === 'status') {
1621
+ const result = store.status(base)
1622
+ if (useJson) return jsonOrText(result, true)
1623
+ return printObserverStatus(result)
1624
+ }
1625
+ if (sub === 'world-model') {
1626
+ const result = store.worldModel(base)
1627
+ if (useJson) return jsonOrText(result, true)
1628
+ return printObserverWorldModel(result)
1629
+ }
1630
+ if (sub === 'trends') {
1631
+ const result = store.trends({ ...base, date: optionValue(flags, '--date'), limit: parseNumberOption(flags, '--limit') })
1632
+ if (useJson) return jsonOrText(result, true)
1633
+ return printObserverList('trend report(s)', result)
1634
+ }
1635
+ if (sub === 'research') {
1636
+ const result = store.research({ ...base, limit: parseNumberOption(flags, '--limit') })
1637
+ if (useJson) return jsonOrText(result, true)
1638
+ return printObserverList('research report(s)', result)
1639
+ }
1640
+ if (sub === 'insights') {
1641
+ const result = store.insights({ ...base, limit: parseNumberOption(flags, '--limit') })
1642
+ if (useJson) return jsonOrText(result, true)
1643
+ return printObserverList('insight(s)', result)
1644
+ }
1645
+ if (sub === 'essays') {
1646
+ const result = store.essays({ ...base, type: optionValue(flags, '--type'), limit: parseNumberOption(flags, '--limit') })
1647
+ if (useJson) return jsonOrText(result, true)
1648
+ return printObserverEssays(result)
1649
+ }
1650
+ const days = parseNumberOption(flags, '--days')
1651
+ const result = store.dag({ ...base, days })
1652
+ if (useJson) return jsonOrText(result, true)
1653
+ return printObserverDag(result, days)
1654
+ } catch (error) {
1655
+ console.error(error.message || error)
1656
+ process.exitCode = 1
1657
+ }
1658
+ }
1659
+
1660
+ function creativityUsage() {
1661
+ console.log(`Usage:
1662
+ mio creativity status Show hypothesis counts and recent top ideas
1663
+ mio creativity list List hypotheses (--status active|validated|rejected|draft, --limit N)
1664
+
1665
+ mio creativity generate Generate hypotheses by combining sources (needs 2+ --source, calls an LLM)
1666
+ mio creativity ferment Review/refine active hypotheses (calls an LLM)
1667
+
1668
+ Options:
1669
+ --status name Filter list by status
1670
+ --limit N Max results for list / ferment (default 20 / 5)
1671
+ --source "name|content" A concept source for generate; repeat 2+ times
1672
+ --strategy explore|signal|stable Generation strategy (auto-selected if omitted)
1673
+ --json Machine-readable output (same shape as the mio.creativity.* tools)
1674
+
1675
+ Examples:
1676
+ mio creativity status
1677
+ mio creativity list --status rejected --limit 10
1678
+ mio creativity generate --source "auth|token rotation" --source "cache|write-through"
1679
+
1680
+ generate/ferment call an LLM using the shared client. Configure it with
1681
+ LLM_API_URL / LLM_KEY / LLM_CHAT_MODEL (the same environment the MCP server
1682
+ reads). A local Ollama works:
1683
+ LLM_API_URL=http://localhost:11434/v1/chat/completions`)
1684
+ }
1685
+
1686
+ function printCreativityStatus(result) {
1687
+ console.log('Creativity engine:')
1688
+ console.log(` hypotheses: ${result.hypotheses} combos: ${result.combos} experiments: ${result.experiments}`)
1689
+ console.log(` active: ${result.active} validated: ${result.validated} rejected: ${result.rejected}`)
1690
+ if (result.recentIdeas.length === 0) {
1691
+ console.log('No hypotheses yet.')
1692
+ return
1693
+ }
1694
+ console.log('\nRecent top ideas:')
1695
+ for (const idea of result.recentIdeas) {
1696
+ console.log(`- ${idea.title} (novelty=${idea.novelty} feasibility=${idea.feasibility} impact=${idea.impact} score=${idea.score})`)
1697
+ console.log(` ${idea.id}`)
1698
+ }
1699
+ }
1700
+
1701
+ function printCreativityList(items) {
1702
+ if (items.length === 0) {
1703
+ console.log('No hypotheses match.')
1704
+ return
1705
+ }
1706
+ console.log(`Hypotheses: ${items.length} shown`)
1707
+ items.forEach((h, index) => {
1708
+ const when = h.createdAt ? new Date(h.createdAt).toISOString().slice(0, 19).replace('T', ' ') : ''
1709
+ const labels = Array.isArray(h.sourceLabels) && h.sourceLabels.length > 0 ? h.sourceLabels.join(' + ') : '—'
1710
+ console.log(`${index + 1}. [${h.status}] ${h.title} (N=${h.novelty} F=${h.feasibility} I=${h.impact} score=${h.score})`)
1711
+ console.log(` ${labels} | ${when} | ${h.id}`)
1712
+ if (h.fermentCount) console.log(` fermented ${h.fermentCount}x`)
1713
+ })
1714
+ }
1715
+
1716
+ // Parses repeated `--source "name|content"` into the shape CreativityEngine
1717
+ // expects. At least two are required: the engine pairs up sources, so one
1718
+ // source can never produce a combination.
1719
+ function parseSources(flags) {
1720
+ const sources = []
1721
+ for (let i = 0; i < flags.length; i += 1) {
1722
+ if (flags[i] !== '--source') continue
1723
+ const raw = flags[i + 1]
1724
+ if (!raw) continue
1725
+ const sep = raw.indexOf('|')
1726
+ if (sep === -1) {
1727
+ sources.push({ name: raw, content: raw })
1728
+ } else {
1729
+ sources.push({
1730
+ name: raw.slice(0, sep).trim(),
1731
+ content: raw.slice(sep + 1).trim(),
1732
+ })
1733
+ }
1734
+ }
1735
+ return sources
1736
+ }
1737
+
1738
+ function warnIfLlmUnconfigured() {
1739
+ if (isLlmConfigured()) return
1740
+ const { apiUrl, model } = llmConfig()
1741
+ console.log(`Note: no LLM_API_URL / LLM_KEY set, so this will call the default endpoint (${model} @ ${apiUrl}).`)
1742
+ console.log('Set LLM_API_URL (e.g. http://localhost:11434/v1/chat/completions for a local Ollama) to point elsewhere.')
1743
+ }
1744
+
1745
+ function creativityGenerateCommand(args, useJson) {
1746
+ const flags = args.slice(2)
1747
+ const sources = parseSources(flags)
1748
+ if (sources.length < 2) {
1749
+ console.error('mio creativity generate requires at least two --source "name|content" arguments')
1750
+ console.error('Example: mio creativity generate --source "auth|token rotation" --source "cache|write-through cache"')
1751
+ process.exitCode = 1
1752
+ return
1753
+ }
1754
+ const strategy = optionValue(flags, '--strategy')
1755
+
1756
+ if (!useJson) warnIfLlmUnconfigured()
1757
+
1758
+ let result
1759
+ try {
1760
+ result = cliCreativityEngine().generate(sources, strategy)
1761
+ } catch (error) {
1762
+ console.error(error.message || error)
1763
+ process.exitCode = 1
1764
+ return
1765
+ }
1766
+ // generate is async in the engine.
1767
+ return Promise.resolve(result).then((resolved) => {
1768
+ if (useJson) return jsonOrText(resolved, true)
1769
+ if (resolved.reason) {
1770
+ console.log(`No hypotheses generated: ${resolved.reason}`)
1771
+ return
1772
+ }
1773
+ console.log(`Generated ${resolved.ideas.length} hypothesis/hypotheses (strategy=${resolved.strategy})`)
1774
+ resolved.ideas.forEach((idea, index) => {
1775
+ console.log(`${index + 1}. [${idea.status}] ${idea.title}`)
1776
+ console.log(` novelty=${idea.novelty} feasibility=${idea.feasibility} impact=${idea.impact} | ${idea.id}`)
1777
+ if (idea.rejectionReason) console.log(` rejected: ${idea.rejectionReason}`)
1778
+ })
1779
+ })
1780
+ }
1781
+
1782
+ function creativityFermentCommand(args, useJson) {
1783
+ const flags = args.slice(2)
1784
+ const limit = parseNumberOption(flags, '--limit')
1785
+
1786
+ if (!useJson) warnIfLlmUnconfigured()
1787
+
1788
+ let result
1789
+ try {
1790
+ result = cliCreativityEngine().ferment(limit)
1791
+ } catch (error) {
1792
+ console.error(error.message || error)
1793
+ process.exitCode = 1
1794
+ return
1795
+ }
1796
+ return Promise.resolve(result).then((resolved) => {
1797
+ if (useJson) return jsonOrText(resolved, true)
1798
+ if (resolved.fermented === 0) {
1799
+ console.log(`Nothing fermented: ${resolved.reason || 'no eligible hypotheses'}`)
1800
+ return
1801
+ }
1802
+ console.log(`Fermented ${resolved.fermented} hypothesis/hypotheses`)
1803
+ for (const r of resolved.results || []) {
1804
+ console.log(`- ${r.title || r.id}: ${r.verdict || 'updated'}${r.reason ? ` — ${r.reason}` : ''}`)
1805
+ }
1806
+ })
1807
+ }
1808
+
1809
+ function creativityCommand(args, useJson) {
1810
+ const sub = args[1]
1811
+
1812
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
1813
+ creativityUsage()
1814
+ if (!sub) process.exitCode = 1
1815
+ return
1816
+ }
1817
+ if (sub === 'generate') return creativityGenerateCommand(args, useJson)
1818
+ if (sub === 'ferment') return creativityFermentCommand(args, useJson)
1819
+
1820
+ if (!['status', 'list'].includes(sub)) {
1821
+ console.error(`Unknown creativity subcommand: ${sub}`)
1822
+ creativityUsage()
1823
+ process.exitCode = 1
1824
+ return
1825
+ }
1826
+
1827
+ const flags = args.slice(2)
1828
+ const engine = cliCreativityEngine()
1829
+
1830
+ let result
1831
+ try {
1832
+ if (sub === 'status') {
1833
+ result = engine.status()
1834
+ } else {
1835
+ result = engine.list({
1836
+ status: optionValue(flags, '--status'),
1837
+ limit: parseNumberOption(flags, '--limit'),
1838
+ })
1839
+ }
1840
+ } catch (error) {
1841
+ console.error(error.message || error)
1842
+ process.exitCode = 1
1843
+ return
1844
+ }
1845
+
1846
+ if (useJson) return jsonOrText(result, true)
1847
+ if (sub === 'status') printCreativityStatus(result)
1848
+ else printCreativityList(result)
1849
+ }
1850
+
1851
+ function readConfig() {
1852
+ const fallback = { version: 1, home: MIO_HOME, agents: {} }
1853
+ if (!fs.existsSync(CONFIG_FILE)) return fallback
1854
+ try {
1855
+ return { ...fallback, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
1856
+ } catch (_) {
1857
+ return fallback
1858
+ }
1859
+ }
1860
+
1861
+ function writeConfig(config) {
1862
+ ensureHome()
1863
+ fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
1864
+ }
1865
+
1866
+ function jsonOrText(payload, useJson) {
1867
+ if (useJson) {
1868
+ console.log(JSON.stringify(payload, null, 2))
1869
+ return
1870
+ }
1871
+ return payload
1872
+ }
1873
+
1874
+ function initCommand() {
1875
+ ensureHome()
1876
+ const config = readConfig()
1877
+ if (!config.createdAt) config.createdAt = new Date().toISOString()
1878
+ writeConfig(config)
1879
+ console.log(`MIO_HOME ready: ${MIO_HOME}`)
1880
+ }
1881
+
1882
+ function statusCommand(useJson) {
1883
+ const config = readConfig()
1884
+ const codexRestart = adapters.codex.restartWarningMessage()
1885
+ const opencodeRestart = adapters.opencode.restartWarningMessage()
1886
+ const installed = {
1887
+ codex: adapters.codex.isInstalled(),
1888
+ opencode: adapters.opencode.isInstalled(),
1889
+ workbuddy: adapters.workbuddy.isInstalled(),
1890
+ hermes: adapters.hermes.isInstalled(),
1891
+ claude: adapters.claude.isInstalled(),
1892
+ }
1893
+ const payload = {
1894
+ version: config.version,
1895
+ home: MIO_HOME,
1896
+ serverScript: SERVER_SCRIPT,
1897
+ serverScriptExists: fs.existsSync(SERVER_SCRIPT),
1898
+ agents: config.agents || {},
1899
+ codexInstalled: installed.codex,
1900
+ opencodeInstalled: installed.opencode,
1901
+ workbuddyInstalled: installed.workbuddy,
1902
+ hermesInstalled: installed.hermes,
1903
+ claudeInstalled: installed.claude,
1904
+ codexNeedsRestart: Boolean(codexRestart),
1905
+ opencodeNeedsRestart: Boolean(opencodeRestart),
1906
+ codexRestartHint: codexRestart,
1907
+ opencodeRestartHint: opencodeRestart,
1908
+ hermesObserverReady: fs.existsSync(observer.hermesDbPath()),
1909
+ claudeObserverReady: fs.existsSync(observer.claudeProjectsDir()),
1910
+ opencodeObserverReady: fs.existsSync(observer.opencodeDbPath()),
1911
+ observerRunning: observer.isRunning(MIO_HOME),
1912
+ observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
1913
+ }
1914
+ if (useJson) {
1915
+ jsonOrText(payload, true)
1916
+ return
1917
+ }
1918
+ console.log(`MIO_HOME: ${MIO_HOME}`)
1919
+ console.log(`MCP Server: ${SERVER_SCRIPT}`)
1920
+ console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
1921
+ if (codexRestart) console.log(`Codex app: running with an older Mio config - fully restart Codex to load it`)
1922
+ console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
1923
+ if (opencodeRestart) console.log(`OpenCode Desktop: running with an older Mio config - restart it to load it`)
1924
+ console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
1925
+ console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
1926
+ console.log(`Hermes MCP: ${installed.hermes ? 'installed' : 'not installed'}`)
1927
+ console.log(`Hermes observer: ${fs.existsSync(observer.hermesDbPath()) ? 'ready' : 'no database'}`)
1928
+ console.log(`Claude Code MCP: ${installed.claude ? 'installed' : 'not installed'}`)
1929
+ console.log(`Claude Code observer: ${fs.existsSync(observer.claudeProjectsDir()) ? 'ready' : 'no transcripts'}`)
1930
+ console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
1931
+ }
1932
+
1933
+ function agentsCommand(args, useJson) {
1934
+ const sub = args[1]
1935
+
1936
+ // `mio agents` with no subcommand lists installed host adapters (config) —
1937
+ // distinct from the observed-agent telemetry below.
1938
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
1939
+ const config = readConfig()
1940
+ const agents = config.agents || {}
1941
+ if (useJson) {
1942
+ jsonOrText(agents, true)
1943
+ return
1944
+ }
1945
+ const names = Object.keys(agents)
1946
+ if (names.length === 0) {
1947
+ console.log('No agents installed. Run `mio agents list` for observed agents, or `mio install <host>` to add one.')
1948
+ return
1949
+ }
1950
+ for (const name of names) {
1951
+ console.log(`${name}\t${agents[name].installedAt || ''}`)
1952
+ }
1953
+ return
1954
+ }
1955
+
1956
+ if (sub === 'list') {
1957
+ const flags = args.slice(2)
1958
+ let result
1959
+ try {
1960
+ result = cliAgentStore().listAgents({ project: optionValue(flags, '--project') })
1961
+ } catch (error) {
1962
+ console.error(error.message || error)
1963
+ process.exitCode = 1
1964
+ return
1965
+ }
1966
+ if (useJson) return jsonOrText(result, true)
1967
+ return printAgentList(result)
1968
+ }
1969
+
1970
+ if (sub === 'report') {
1971
+ const flags = args.slice(2)
1972
+ let result
1973
+ try {
1974
+ result = cliAgentStore().reportAgent({
1975
+ agentId: optionValue(flags, '--agent'),
1976
+ project: optionValue(flags, '--project'),
1977
+ })
1978
+ } catch (error) {
1979
+ console.error(error.message || error)
1980
+ process.exitCode = 1
1981
+ return
1982
+ }
1983
+ if (useJson) return jsonOrText(result, true)
1984
+ return printAgentReport(result)
1985
+ }
1986
+
1987
+ if (sub === 'register') {
1988
+ const flags = args.slice(2)
1989
+ const agentId = optionValue(flags, '--agent-id')
1990
+ if (!agentId) {
1991
+ console.error('mio agents register requires --agent-id <id>')
1992
+ process.exitCode = 1
1993
+ return
1994
+ }
1995
+ const project = optionValue(flags, '--project') || projectName()
1996
+ const hostType = optionValue(flags, '--host-type') || 'mcp'
1997
+ const capabilities = splitTagsOption(flags, '--capabilities')
1998
+
1999
+ // Preview by default, like `mio memory archive`: nothing is written
2000
+ // without --yes, in --json mode either.
2001
+ if (!flagPresent(flags, '--yes')) {
2002
+ const existing = cliAgentStore()
2003
+ .listAgents({ project })
2004
+ .agents.find((a) => a.agentId === agentId)
2005
+ if (useJson) {
2006
+ jsonOrText(
2007
+ {
2008
+ preview: true,
2009
+ applied: false,
2010
+ agentId,
2011
+ project,
2012
+ hostType,
2013
+ capabilities,
2014
+ willCreate: !existing,
2015
+ existingSessionCount: existing ? existing.sessionCount || 0 : null,
2016
+ hint: 'Re-run with --yes to apply.',
2017
+ },
2018
+ true,
2019
+ )
2020
+ } else {
2021
+ console.log(`Register preview: ${agentId} (project=${project}, host=${hostType})`)
2022
+ if (existing) {
2023
+ const next = (existing.sessionCount || 0) + 1
2024
+ console.log(` will update existing agent (sessionCount ${existing.sessionCount || 0} -> ${next})`)
2025
+ } else {
2026
+ console.log(' will create a new agent record')
2027
+ }
2028
+ if (capabilities.length > 0) console.log(` capabilities: ${capabilities.join(', ')}`)
2029
+ console.log('Re-run with --yes to apply.')
2030
+ }
2031
+ process.exitCode = 1
2032
+ return
2033
+ }
2034
+
2035
+ let result
2036
+ try {
2037
+ result = cliAgentStore().registerAgent({ agentId, project, hostType, capabilities })
2038
+ } catch (error) {
2039
+ console.error(error.message || error)
2040
+ process.exitCode = 1
2041
+ return
2042
+ }
2043
+ if (useJson) return jsonOrText(result, true)
2044
+ console.log(`Registered ${result.agentId} (project=${project})`)
2045
+ console.log(
2046
+ ` ${result.registered ? 'created' : `updated (session ${result.sessionCount})`} | host=${result.hostType}`,
2047
+ )
2048
+ if (result.capabilities && result.capabilities.length > 0) {
2049
+ console.log(` capabilities: ${result.capabilities.join(', ')}`)
2050
+ }
2051
+ return
2052
+ }
2053
+
2054
+ console.error(`Unknown agents subcommand: ${sub}`)
2055
+ console.error('Usage: mio agents [list|register|report] [--agent X] [--project Y]')
2056
+ process.exitCode = 1
2057
+ }
2058
+
2059
+ function printAgentList(result) {
2060
+ console.log(`Observed agents: ${result.count} shown${result.project ? ` (project=${result.project})` : ''}`)
2061
+ if (result.count === 0) {
2062
+ console.log('No observed agents match.')
2063
+ return
2064
+ }
2065
+ for (const a of result.agents) {
2066
+ const caps = Array.isArray(a.capabilities) && a.capabilities.length > 0 ? ` [${a.capabilities.join(', ')}]` : ''
2067
+ console.log(`${a.agentId} (${a.hostType})${caps} project=${a.project || '-'}`)
2068
+ console.log(
2069
+ ` sessions=${a.sessionCount || 0} tasks=${a.taskCount} success=${a.successCount} failure=${a.failureCount} | ${a.id}`,
2070
+ )
2071
+ }
2072
+ }
2073
+
2074
+ function printAgentReport(result) {
2075
+ console.log(`Agent report${result.project ? ` (project=${result.project})` : ''}: ${result.count} agent(s)`)
2076
+ if (result.count === 0) {
2077
+ console.log('No observed agents match.')
2078
+ return
2079
+ }
2080
+ for (const r of result.reports) {
2081
+ const o = r.taskOutcomes
2082
+ const when = r.lastSeenAt ? r.lastSeenAt.slice(0, 19).replace('T', ' ') : 'unknown'
2083
+ console.log(`${r.agentId} (${r.hostType}) ${r.active ? 'active' : 'idle'}`)
2084
+ console.log(
2085
+ ` tasks: ${o.total} total, ${o.success} success, ${o.failure} failure (${o.successRate}% success)`,
2086
+ )
2087
+ console.log(` memories: ${r.memories} experience reuses: ${r.experienceReuses.total} (verified ${r.experienceReuses.verified})`)
2088
+ console.log(` last seen: ${when} | sessions=${r.sessionCount}`)
2089
+ }
2090
+ }
2091
+
2092
+ function phase0Command(args, useJson) {
2093
+ const sub = args[1]
2094
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
2095
+ console.log(`Usage:
2096
+ mio phase0 report Show the Phase 0 validation report (memory/trace/reuse evidence)
2097
+ mio phase0 report --format markdown Render as Markdown
2098
+ mio phase0 report --json Machine-readable report (same shape as mio.phase0.report)
2099
+
2100
+ Options:
2101
+ --project name Project filter (defaults to the current directory name)
2102
+ --format markdown Human-readable Markdown instead of JSON`)
2103
+ if (!sub) process.exitCode = 1
2104
+ return
2105
+ }
2106
+ if (sub !== 'report') {
2107
+ console.error(`Unknown phase0 subcommand: ${sub}`)
2108
+ process.exitCode = 1
2109
+ return
2110
+ }
2111
+
2112
+ const flags = args.slice(2)
2113
+ const format = (optionValue(flags, '--format') || '').toLowerCase()
2114
+ let report
2115
+ try {
2116
+ report = loadPhase0(MIO_HOME, optionValue(flags, '--project') || projectName())
2117
+ } catch (error) {
2118
+ console.error(error.message || error)
2119
+ process.exitCode = 1
2120
+ return
2121
+ }
2122
+
2123
+ if (useJson) {
2124
+ return jsonOrText({ project: optionValue(flags, '--project') || projectName(), ...report }, true)
2125
+ }
2126
+ // Markdown is the human-readable form; the MCP tool exposes the same renderer
2127
+ // via --format markdown, so text and MCP output cannot diverge.
2128
+ console.log(renderPhase0Markdown(report))
2129
+ }
2130
+
2131
+ function hostCommand(args, useJson) {
2132
+ const sub = args[1]
2133
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
2134
+ console.log(`Usage:
2135
+ mio host capabilities Show what each host supports and whether it is installed
2136
+ mio host capabilities --json Machine-readable (same shape as mio.host.capabilities)
2137
+
2138
+ Notes:
2139
+ "installed" is probed live from each adapter, so it reflects this machine right
2140
+ now -- unlike \`mio install <host>\`, which only records intent in config.json.`)
2141
+ if (!sub) process.exitCode = 1
2142
+ return
2143
+ }
2144
+ if (sub !== 'capabilities') {
2145
+ console.error(`Unknown host subcommand: ${sub}`)
2146
+ process.exitCode = 1
2147
+ return
2148
+ }
2149
+
2150
+ const result = listHostCapabilities()
2151
+ if (useJson) return jsonOrText(result, true)
2152
+
2153
+ console.log(`Host capabilities: ${result.hosts.length} host(s)`)
2154
+ for (const host of result.hosts) {
2155
+ const mark = host.installed ? 'installed' : 'not installed'
2156
+ console.log(` ${host.name.padEnd(10)} ${mark}`)
2157
+ console.log(` ${host.capabilities.join(', ')}`)
2158
+ }
2159
+ }
2160
+
2161
+ function recordOutcomeCommand(args, useJson) {
2162
+ const flags = args.slice(2)
2163
+ const outcome = String(optionValue(flags, '--outcome') || '').toLowerCase()
2164
+ if (!['success', 'failure', 'aborted'].includes(outcome)) {
2165
+ console.error('mio task record-outcome requires --outcome success|failure|aborted')
2166
+ process.exitCode = 1
2167
+ return
2168
+ }
2169
+ const project = optionValue(flags, '--project') || projectName()
2170
+ // Mirrors the store default so the preview reports the same agent that the
2171
+ // applied run would use.
2172
+ const agentId = optionValue(flags, '--agent-id') || 'cli'
2173
+ const task = optionValue(flags, '--task') || ''
2174
+ const summary = optionValue(flags, '--summary') || ''
2175
+ const verification = optionValue(flags, '--verification') || ''
2176
+ const traceId = optionValue(flags, '--trace-id') || ''
2177
+
2178
+ // Preview by default, like every other CLI write (mio memory archive,
2179
+ // mio agents register): nothing is written without --yes, --json included.
2180
+ if (!flagPresent(flags, '--yes')) {
2181
+ const existing = cliAgentStore()
2182
+ .listAgents({ project })
2183
+ .agents.find((a) => a.agentId === agentId)
2184
+ if (useJson) {
2185
+ jsonOrText(
2186
+ {
2187
+ preview: true,
2188
+ applied: false,
2189
+ outcome,
2190
+ agentId,
2191
+ project,
2192
+ task,
2193
+ summary,
2194
+ verification,
2195
+ agentWillUpdate: !!existing,
2196
+ currentTaskCount: existing ? existing.taskCount || 0 : null,
2197
+ hint: 'Re-run with --yes to apply.',
2198
+ },
2199
+ true,
2200
+ )
2201
+ } else {
2202
+ console.log(`Record outcome preview: ${outcome} (project=${project}, agent=${agentId})`)
2203
+ if (task) console.log(` task: ${task}`)
2204
+ if (summary) console.log(` summary: ${summary}`)
2205
+ if (existing) {
2206
+ const nextTasks = (existing.taskCount || 0) + 1
2207
+ const nextSuccess = (existing.successCount || 0) + (outcome === 'success' ? 1 : 0)
2208
+ console.log(
2209
+ ` will update agent (taskCount ${existing.taskCount || 0} -> ${nextTasks}, successCount ${existing.successCount || 0} -> ${nextSuccess})`,
2210
+ )
2211
+ } else {
2212
+ console.log(` agent not registered: trace only, agent registry untouched`)
2213
+ console.log(` register it with: mio agents register --agent-id ${agentId} --yes`)
2214
+ }
2215
+ console.log('Re-run with --yes to apply.')
2216
+ }
2217
+ process.exitCode = 1
2218
+ return
2219
+ }
2220
+
2221
+ let result
2222
+ try {
2223
+ result = cliTaskStore().recordTaskOutcome({
2224
+ outcome,
2225
+ agentId,
2226
+ project,
2227
+ task,
2228
+ summary,
2229
+ verification,
2230
+ traceId,
2231
+ })
2232
+ } catch (error) {
2233
+ console.error(error.message || error)
2234
+ process.exitCode = 1
2235
+ return
2236
+ }
2237
+
2238
+ if (useJson) return jsonOrText(result, true)
2239
+ console.log(`Recorded ${result.event.outcome} for ${result.event.agent} (project=${result.event.project})`)
2240
+ console.log(` trace: ${result.event.trace_id}`)
2241
+ console.log(` agent registry: ${result.agentUpdated ? 'updated' : 'not updated (agent not registered)'}`)
2242
+ if (result.autoClaims) console.log(` auto-claims: ${result.autoClaims.length}`)
2243
+ }
2244
+
2245
+ function taskCommand(args, useJson) {
2246
+ const sub = args[1]
2247
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
2248
+ console.log(`Usage:
2249
+ mio task route "<task>" Which verified experiences apply to this task
2250
+ mio task route "<task>" --json Machine-readable (same shape as mio.task.route)
2251
+
2252
+ Options:
2253
+ --project name Project filter (defaults to the current directory name)
2254
+ --scope all|project|global Memory scope to consider
2255
+ --limit N Max routes (1-10, default 5)
2256
+
2257
+ This is read-only: unlike the MCP call it does not write to the query log.`)
2258
+ if (!sub) process.exitCode = 1
2259
+ return
2260
+ }
2261
+ if (sub === 'record-outcome' || sub === 'record_outcome') {
2262
+ return recordOutcomeCommand(args, useJson)
2263
+ }
2264
+
2265
+ if (sub !== 'route') {
2266
+ console.error(`Unknown task subcommand: ${sub}`)
2267
+ process.exitCode = 1
2268
+ return
2269
+ }
2270
+
2271
+ const flags = args.slice(2)
2272
+ // The task is free text and may be quoted as one argument or passed as
2273
+ // several words. Only flags this command understands are treated as options.
2274
+ const TASK_KNOWN_FLAGS = new Set(['--project', '--scope', '--limit', '--json'])
2275
+ const taskParts = []
2276
+ for (let i = 0; i < flags.length; i += 1) {
2277
+ const token = flags[i]
2278
+ if (TASK_KNOWN_FLAGS.has(token)) continue
2279
+ if (i > 0 && TASK_KNOWN_FLAGS.has(flags[i - 1])) continue
2280
+ taskParts.push(token)
2281
+ }
2282
+ const task = taskParts.join(' ').trim()
2283
+ if (!task) {
2284
+ console.error('mio task route requires a task, e.g. mio task route "publish the npm package"')
2285
+ process.exitCode = 1
2286
+ return
2287
+ }
2288
+
2289
+ let result
2290
+ try {
2291
+ result = cliTaskStore().routeTask({
2292
+ task,
2293
+ project: optionValue(flags, '--project'),
2294
+ scope: optionValue(flags, '--scope'),
2295
+ limit: parseNumberOption(flags, '--limit'),
2296
+ persistQuery: false,
2297
+ })
2298
+ } catch (error) {
2299
+ console.error(error.message || error)
2300
+ process.exitCode = 1
2301
+ return
2302
+ }
2303
+
2304
+ if (useJson) return jsonOrText(result, true)
2305
+ printTaskRoute(result)
2306
+ }
2307
+
2308
+ function printTaskRoute(result) {
2309
+ console.log(`Task route: "${result.task}" (project=${result.project || 'all'}, scope=${result.scope})`)
2310
+ console.log(`verified routes: ${result.count}`)
2311
+
2312
+ if (result.count > 0) {
2313
+ console.log('')
2314
+ console.log('Routes (apply the top match first):')
2315
+ result.routes.forEach((route, index) => {
2316
+ const via = route.sourceAgents.length > 0 ? route.sourceAgents.join(', ') : '?'
2317
+ const to = route.targetAgents.length > 0 ? route.targetAgents.join(', ') : '?'
2318
+ console.log(
2319
+ `${index + 1}. [score ${route.score}] ${route.experienceId} — reused ${route.reuseCount}x${route.confirmed ? ' (confirmed)' : ''}`,
2320
+ )
2321
+ console.log(` ${String(route.memory.content).replace(/\s+/g, ' ').slice(0, 110)}`)
2322
+ console.log(` ${via} -> ${to}`)
2323
+ })
2324
+ }
2325
+
2326
+ if (result.relatedMemories.length > 0) {
2327
+ console.log('')
2328
+ console.log('Related memories:')
2329
+ for (const record of result.relatedMemories) {
2330
+ console.log(`- ${record.id} ${String(record.content).replace(/\s+/g, ' ').slice(0, 100)}`)
2331
+ }
2332
+ }
2333
+
2334
+ if (result.summary && result.summary.routingSignal) {
2335
+ console.log(`\nAgent health: ${result.summary.routingSignal}`)
2336
+ }
2337
+ if (result.summary && result.summary.suggestion) {
2338
+ console.log(`\n${result.summary.suggestion}`)
550
2339
  }
551
2340
  }
552
2341
 
@@ -656,8 +2445,12 @@ Usage:
656
2445
  mio mcp Start Mio MCP server (stdio)
657
2446
  mio install <host> Install Mio into a host (codex|opencode|workbuddy|hermes|claude)
658
2447
  mio status Show runtime and adapter status
659
- mio agents List installed agents
2448
+ mio agents List installed host adapters
2449
+ mio agents list List observed agents (from agents.jsonl, --project X)
2450
+ mio agents report Report per-agent task/memory/reuse telemetry (--agent X, --project Y)
2451
+ mio agents register --agent-id X Register an observed agent (--yes to apply; previews by default)
660
2452
  mio evolution status Show composed evolution module health
2453
+ mio evolution report Cross-agent evolution report: ecosystem, agents, memory health, suggestions (--period 24h|7d|30d|all)
661
2454
  mio evolution shadow record Record a shadow comparison sample
662
2455
  mio evolution dual-write record Record a dual-write comparison sample
663
2456
  mio evolution cutover readiness Assess shadow/dual-write cutover readiness
@@ -669,6 +2462,32 @@ Usage:
669
2462
  mio recall "<query>" Search Mio memory from the terminal (same ranking as mio.memory.query)
670
2463
  mio traces Show recent observer traces (--type/--outcome/--agent/--since/--limit/--compact)
671
2464
  mio remember "<content>" Write a memory record from the terminal (same schema as mio.memory.record)
2465
+ mio memory analyze Report duplicates, low-quality records and kind histogram
2466
+ mio memory archive --ids a,b Archive records (soft delete; --yes required to apply)
2467
+ mio memory forget --ids a,b PERMANENTLY delete records (--yes required; not undoable)
2468
+ mio memory restore --ids a,b Un-archive previously archived records
2469
+ mio memory merge --ids a,b Merge duplicates into one survivor (--yes required to apply)
2470
+ mio memory migrate --ids a,b --scope global|project Move records between layers
2471
+ mio experience list List experience reuse (--status pending|confirmed|verified)
2472
+ mio experience confirm --ids a,b Confirm auto-claimed reuse (bulk supported)
2473
+ mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse
2474
+ mio creativity status Show creativity hypothesis counts and recent top ideas
2475
+ mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --limit N)
2476
+ mio creativity generate Generate hypotheses from 2+ --source "name|content" (calls an LLM)
2477
+ mio creativity ferment Review and refine active hypotheses (calls an LLM)
2478
+ mio insight status Insight counts: total, reported, unreported, high-value
2479
+ mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
2480
+ mio insight mark-reported Mark insights as reported (--ids a,b)
2481
+ mio observer <view> Observer pipeline views (research pipeline, not the observe daemon):
2482
+ status | world-model | trends | research | insights | essays | dag
2483
+ mio observer ingest --trace-id T --event-type E Record a trace event (--payload/--outcome)
2484
+ mio observer subscribe --event-types a,b Subscribe to events (--yes to apply)
2485
+ mio observer digest New events since the last digest (advances cursor)
2486
+ mio phase0 report Show the Phase 0 validation report (--project X, --format markdown)
2487
+ mio host capabilities Show what each host supports and whether it is installed
2488
+ mio task route "<task>" Which verified experiences apply to this task (--project/--scope/--limit)
2489
+ mio task record-outcome --outcome success Record a task outcome (--yes to apply; previews by default)
2490
+ mio policy check "<action>" Check historical risk for an action before running it
672
2491
  mio prune --days 30 Trim old traces/queries/reuse records and observe.log (--dry-run to preview; --memory needs --yes)
673
2492
  mio digest --days 7 Aggregate traces/memory/reuse into an actionable report (--write-back feeds agent context files; --json)
674
2493
  mio --json status Machine-readable status
@@ -694,7 +2513,7 @@ async function main() {
694
2513
  case 'status':
695
2514
  return statusCommand(useJson)
696
2515
  case 'agents':
697
- return agentsCommand(useJson)
2516
+ return agentsCommand(args, useJson)
698
2517
  case 'observe':
699
2518
  return observeCommand(args)
700
2519
  case 'recall':
@@ -703,6 +2522,24 @@ async function main() {
703
2522
  return tracesCommand(args, useJson)
704
2523
  case 'remember':
705
2524
  return rememberCommand(args, useJson)
2525
+ case 'memory':
2526
+ return memoryCommand(args, useJson)
2527
+ case 'experience':
2528
+ return experienceCommand(args, useJson)
2529
+ case 'policy':
2530
+ return policyCommand(args, useJson)
2531
+ case 'creativity':
2532
+ return creativityCommand(args, useJson)
2533
+ case 'insight':
2534
+ return insightCommand(args, useJson)
2535
+ case 'observer':
2536
+ return observerCommand(args, useJson)
2537
+ case 'phase0':
2538
+ return phase0Command(args, useJson)
2539
+ case 'host':
2540
+ return hostCommand(args, useJson)
2541
+ case 'task':
2542
+ return taskCommand(args, useJson)
706
2543
  case 'prune':
707
2544
  return pruneCommand(args, useJson)
708
2545
  case 'digest':