mio-agent-runtime 0.5.17 → 0.5.22

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.
@@ -86,6 +86,9 @@ function loadState(home) {
86
86
  hmSeeded: parsed.hmSeeded || false,
87
87
  hmLastMsgId: parsed.hmLastMsgId || 0,
88
88
  hmTurns: parsed.hmTurns || {},
89
+ clFiles: parsed.clFiles || {},
90
+ clBuffers: parsed.clBuffers || {},
91
+ clTurns: parsed.clTurns || {},
89
92
  }
90
93
  }
91
94
  } catch (_) {}
@@ -106,6 +109,9 @@ function loadState(home) {
106
109
  hmSeeded: false,
107
110
  hmLastMsgId: 0,
108
111
  hmTurns: {},
112
+ clFiles: {},
113
+ clBuffers: {},
114
+ clTurns: {},
109
115
  }
110
116
  }
111
117
 
@@ -282,9 +288,9 @@ const CONTEXT_MAX = 6
282
288
  // Maintain a compact 'recent Mio context' block inside <workspace>/AGENTS.md,
283
289
  // which WorkBuddy's MemoryLoader auto-injects into every session. This makes
284
290
  // retrieval guaranteed (not dependent on the model deciding to read memory).
285
- function updateProjectContext(state, cwd, line) {
291
+ function updateProjectContext(state, cwd, line, targetFile) {
286
292
  if (!cwd || !line) return
287
- const target = path.join(cwd, 'AGENTS.md')
293
+ const target = path.join(cwd, targetFile || 'AGENTS.md')
288
294
  const list = state.contexts[cwd] || []
289
295
  list.unshift(line)
290
296
  if (list.length > CONTEXT_MAX) list.pop()
@@ -1133,6 +1139,275 @@ function runHermesCycle(home, state, sinks, opts) {
1133
1139
  }
1134
1140
 
1135
1141
 
1142
+ // ---------------------------------------------------------------------------
1143
+ // Claude Code session observation (passive, transcript JSONL tailing)
1144
+ // ---------------------------------------------------------------------------
1145
+
1146
+ // Claude Code writes append-only JSONL transcripts under
1147
+ // <configDir>/projects/<munged-cwd>/<sessionId>.jsonl. Each line carries
1148
+ // {type, message:{role, content}, cwd, sessionId, timestamp, uuid, ...}.
1149
+ // Assistant content items are text/thinking/tool_use; tool results come back
1150
+ // as user lines whose content array holds tool_result items with is_error.
1151
+ // A user prompt line (string content or a text item) closes the previous
1152
+ // turn; sidechain lines (subagent transcripts) are skipped as noise.
1153
+
1154
+ function claudeConfigDir() {
1155
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
1156
+ }
1157
+
1158
+ function claudeProjectsDir() {
1159
+ return path.join(claudeConfigDir(), 'projects')
1160
+ }
1161
+
1162
+ function listClaudeTranscripts() {
1163
+ const root = claudeProjectsDir()
1164
+ const out = []
1165
+ if (!fs.existsSync(root)) return out
1166
+ const walk = (dir) => {
1167
+ let entries
1168
+ try {
1169
+ entries = fs.readdirSync(dir, { withFileTypes: true })
1170
+ } catch (_) {
1171
+ return
1172
+ }
1173
+ for (const entry of entries) {
1174
+ const full = path.join(dir, entry.name)
1175
+ if (entry.isDirectory()) walk(full)
1176
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full)
1177
+ }
1178
+ }
1179
+ walk(root)
1180
+ return out
1181
+ }
1182
+
1183
+ function createClaudeTurn(meta) {
1184
+ meta = meta || {}
1185
+ return {
1186
+ id: null,
1187
+ ts: null,
1188
+ sessionId: meta.sessionId || null,
1189
+ cwd: meta.cwd || null,
1190
+ project: meta.cwd ? path.basename(meta.cwd) : 'unknown',
1191
+ lastText: null,
1192
+ toolCount: 0,
1193
+ errors: [],
1194
+ apiError: false,
1195
+ }
1196
+ }
1197
+
1198
+ function claudeContentItems(obj) {
1199
+ const content = obj && obj.message && obj.message.content
1200
+ if (typeof content === 'string') return [{ type: 'text', text: content }]
1201
+ return Array.isArray(content) ? content.filter(Boolean) : []
1202
+ }
1203
+
1204
+ function finishClaudeTurn(turn, outcome) {
1205
+ return {
1206
+ id: turn.id,
1207
+ ts: turn.ts,
1208
+ sessionId: turn.sessionId,
1209
+ cwd: turn.cwd,
1210
+ project: turn.project,
1211
+ lastText: turn.lastText,
1212
+ toolCount: turn.toolCount,
1213
+ errors: turn.errors.slice(),
1214
+ apiError: turn.apiError,
1215
+ outcome: outcome,
1216
+ }
1217
+ }
1218
+
1219
+ function feedClaudeLine(turn, obj) {
1220
+ if (!obj || typeof obj !== 'object') return { next: turn }
1221
+ // Subagent (sidechain) transcripts duplicate main-thread work; skip them.
1222
+ if (obj.isSidechain === true) return { next: turn }
1223
+ if (obj.sessionId) turn.sessionId = obj.sessionId
1224
+ if (obj.cwd) {
1225
+ turn.cwd = obj.cwd
1226
+ turn.project = path.basename(obj.cwd)
1227
+ }
1228
+
1229
+ if (obj.type === 'user') {
1230
+ const items = claudeContentItems(obj)
1231
+ let hasToolError = false
1232
+ for (const item of items) {
1233
+ if (item.type === 'tool_result' && item.is_error) {
1234
+ turn.errors.push({ toolUseId: item.tool_use_id || 'unknown' })
1235
+ hasToolError = true
1236
+ }
1237
+ }
1238
+ if (turn.errors.length > 20) turn.errors.splice(0, turn.errors.length - 20)
1239
+
1240
+ // tool_result-only user lines continue the current turn; a string content
1241
+ // or text item means a fresh prompt, which closes the previous turn.
1242
+ const isPrompt =
1243
+ typeof (obj.message && obj.message.content) === 'string' ||
1244
+ items.some((item) => item.type === 'text')
1245
+ if (isPrompt) {
1246
+ let finalized = null
1247
+ if (turn.id) {
1248
+ finalized = finishClaudeTurn(turn, turn.apiError ? 'failure' : turn.lastText ? 'success' : 'aborted')
1249
+ }
1250
+ const next = createClaudeTurn({ sessionId: turn.sessionId, cwd: turn.cwd })
1251
+ next.id = obj.uuid || null
1252
+ next.ts = obj.timestamp ? Date.parse(obj.timestamp) || Date.now() : Date.now()
1253
+ return { finalized, next }
1254
+ }
1255
+ return { next: turn }
1256
+ }
1257
+
1258
+ if (obj.type === 'assistant') {
1259
+ for (const item of claudeContentItems(obj)) {
1260
+ if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
1261
+ turn.lastText = item.text
1262
+ } else if (item.type === 'tool_use') {
1263
+ turn.toolCount += 1
1264
+ }
1265
+ }
1266
+ if (obj.isApiErrorMessage === true || obj.error) turn.apiError = true
1267
+ const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN
1268
+ if (Number.isFinite(ts)) turn.ts = ts
1269
+ return { next: turn }
1270
+ }
1271
+
1272
+ return { next: turn }
1273
+ }
1274
+
1275
+ function emitClaudeOutcome(turn, sinks, state) {
1276
+ if (!turn.id) return false
1277
+ const summary = cleanText(turn.lastText)
1278
+ if (!summary && !turn.apiError) return false
1279
+ const outcome = turn.apiError && turn.outcome !== 'failure' ? 'failure' : turn.outcome
1280
+ const turnToken = String(turn.id).slice(0, 8)
1281
+ const tsToken = turn.ts ? Math.floor(turn.ts / 1000) : Math.floor(Date.now() / 1000)
1282
+
1283
+ sinks.trace({
1284
+ trace_id: 'claude:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + tsToken,
1285
+ event_type: 'task_outcome',
1286
+ outcome: outcome,
1287
+ payload: {
1288
+ host: 'claude-code',
1289
+ project: turn.project,
1290
+ sessionId: turn.sessionId,
1291
+ turnId: turn.id,
1292
+ toolCalls: turn.toolCount,
1293
+ errors: turn.errors.length,
1294
+ summary: summary.slice(0, 300),
1295
+ source: 'claude-observer',
1296
+ },
1297
+ agent: 'claude-code',
1298
+ host: 'claude-code',
1299
+ project: turn.project,
1300
+ })
1301
+
1302
+ if (outcome === 'success' && summary.length >= 10) {
1303
+ sinks.memory({
1304
+ kind: 'note',
1305
+ content: '[claude-code] 任务完成: ' + summary.slice(0, 160),
1306
+ tags: ['task-outcome', 'claude-code', turn.project],
1307
+ project: turn.project,
1308
+ source: 'claude-observer',
1309
+ })
1310
+ // Claude Code auto-loads <workspace>/CLAUDE.md, so write the context
1311
+ // block there instead of AGENTS.md.
1312
+ if (turn.cwd) {
1313
+ var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
1314
+ updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80), 'CLAUDE.md')
1315
+ }
1316
+ }
1317
+
1318
+ for (const err of turn.errors) {
1319
+ sinks.trace({
1320
+ trace_id: 'claude:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + String(err.toolUseId || 'err').slice(0, 12),
1321
+ event_type: 'error',
1322
+ outcome: 'error',
1323
+ payload: {
1324
+ host: 'claude-code',
1325
+ project: turn.project,
1326
+ sessionId: turn.sessionId,
1327
+ turnId: turn.id,
1328
+ toolUseId: err.toolUseId,
1329
+ },
1330
+ agent: 'claude-code',
1331
+ host: 'claude-code',
1332
+ project: turn.project,
1333
+ })
1334
+ }
1335
+ return true
1336
+ }
1337
+
1338
+ function runClaudeCycle(home, state, sinks, opts) {
1339
+ const out = { files: 0, lines: 0, outcomes: 0 }
1340
+ const files = listClaudeTranscripts()
1341
+ const now = Date.now()
1342
+ const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
1343
+
1344
+ for (const file of files) {
1345
+ out.files += 1
1346
+ let st
1347
+ try {
1348
+ st = fs.statSync(file)
1349
+ } catch (_) {
1350
+ continue
1351
+ }
1352
+ const info = state.clFiles[file] || { size: 0 }
1353
+ if (!state.clFiles[file] && now - st.mtimeMs > maxAgeMs) continue
1354
+ const size = st.size
1355
+ if (size < info.size) {
1356
+ info.size = 0
1357
+ state.clBuffers[file] = ''
1358
+ }
1359
+
1360
+ let turn = state.clTurns[file] || createClaudeTurn()
1361
+
1362
+ if (size > info.size) {
1363
+ let chunk = ''
1364
+ try {
1365
+ const fd = fs.openSync(file, 'r')
1366
+ const buf = Buffer.alloc(size - info.size)
1367
+ fs.readSync(fd, buf, 0, buf.length, info.size)
1368
+ fs.closeSync(fd)
1369
+ chunk = buf.toString('utf8')
1370
+ } catch (_) {
1371
+ continue
1372
+ }
1373
+ const pending = (state.clBuffers[file] || '') + chunk
1374
+ const lines = pending.split(/\r?\n/)
1375
+ state.clBuffers[file] = lines.pop()
1376
+ for (const line of lines) {
1377
+ if (!line.trim()) continue
1378
+ out.lines += 1
1379
+ let obj
1380
+ try {
1381
+ obj = JSON.parse(line)
1382
+ } catch (_) {
1383
+ continue
1384
+ }
1385
+ const result = feedClaudeLine(turn, obj)
1386
+ turn = result.next
1387
+ if (result.finalized) {
1388
+ if (emitClaudeOutcome(result.finalized, sinks, state)) out.outcomes += 1
1389
+ }
1390
+ }
1391
+ state.clFiles[file] = { size: size, mtimeMs: st.mtimeMs }
1392
+ }
1393
+
1394
+ // Idle finalize: the session has answered but no new prompt arrived yet.
1395
+ if (turn.id && turn.lastText && now - st.mtimeMs > IDLE_MS) {
1396
+ if (emitClaudeOutcome(finishClaudeTurn(turn, turn.apiError ? 'failure' : 'success'), sinks, state)) {
1397
+ out.outcomes += 1
1398
+ }
1399
+ state.clTurns[file] = null
1400
+ } else if (turn.id) {
1401
+ state.clTurns[file] = turn
1402
+ } else {
1403
+ state.clTurns[file] = null
1404
+ }
1405
+ }
1406
+
1407
+ return out
1408
+ }
1409
+
1410
+
1136
1411
  // ---------------------------------------------------------------------------
1137
1412
  // cycle
1138
1413
  // ---------------------------------------------------------------------------
@@ -1225,6 +1500,11 @@ function runCycle(home, state, sinks, opts) {
1225
1500
  out.lines += hmRes.lines
1226
1501
  out.outcomes += hmRes.outcomes
1227
1502
 
1503
+ const clRes = runClaudeCycle(home, state, sinks, opts)
1504
+ out.files += clRes.files
1505
+ out.lines += clRes.lines
1506
+ out.outcomes += clRes.outcomes
1507
+
1228
1508
  saveState(home, state)
1229
1509
  return out
1230
1510
  }
@@ -1317,6 +1597,10 @@ module.exports = {
1317
1597
  runOpencodeCycle,
1318
1598
  runHermesCycle,
1319
1599
  hermesDbPath,
1600
+ claudeProjectsDir,
1601
+ runClaudeCycle,
1602
+ createClaudeTurn,
1603
+ feedClaudeLine,
1320
1604
  createSinks,
1321
1605
  startForeground,
1322
1606
  startBackground,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.5.17",
3
+ "version": "0.5.22",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -17,7 +17,7 @@
17
17
  "!server/mio-intelligence-mcp/__tests__"
18
18
  ],
19
19
  "scripts": {
20
- "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check server/runtime-modules.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
20
+ "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check adapters/claude-code.js && node --check server/runtime-modules.js && node --check server/memory-store.js && node --check server/retention.js && node --check server/digest.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
21
21
  "prepack": "npm run check",
22
22
  "verify:pack-install": "node scripts/verify-packed-runtime.js"
23
23
  },
@@ -0,0 +1,266 @@
1
+ 'use strict'
2
+
3
+ // Digest: turn the accumulated MIO_HOME data (traces, memory, reuse evidence)
4
+ // into an actionable report. Deterministic aggregation only — no LLM. The
5
+ // report answers: what got done, where the errors are, which agent performs
6
+ // best, and what deserves attention next.
7
+
8
+ const fs = require('fs')
9
+ const path = require('path')
10
+
11
+ function readJsonl(file) {
12
+ if (!fs.existsSync(file)) return []
13
+ return fs
14
+ .readFileSync(file, 'utf8')
15
+ .split(/\r?\n/)
16
+ .filter(Boolean)
17
+ .map((line) => {
18
+ try {
19
+ return JSON.parse(line)
20
+ } catch (_) {
21
+ return null
22
+ }
23
+ })
24
+ .filter(Boolean)
25
+ }
26
+
27
+ function isVerifiedReuse(record) {
28
+ return (
29
+ (record.reuse === true || record.reuse === 'true') &&
30
+ (record.behaviorChanged === true || record.behaviorChanged === 'true') &&
31
+ (record.outcomeImproved === true || record.outcomeImproved === 'true')
32
+ )
33
+ }
34
+
35
+ function createDigest(options = {}) {
36
+ const home = options.home
37
+ const now = options.now || (() => Date.now())
38
+
39
+ function generate(args = {}) {
40
+ const days = Math.max(1, Number(args.days) || 7)
41
+ const project = args.project ? String(args.project) : null
42
+ const atTime = now()
43
+ const cutoff = atTime - days * 86400000
44
+
45
+ const traces = readJsonl(path.join(home, 'traces.jsonl')).filter((trace) => {
46
+ if (!trace || typeof trace !== 'object') return false
47
+ const ts = Date.parse(trace.timestamp || '')
48
+ if (!(Number.isFinite(ts) && ts >= cutoff)) return false
49
+ if (project && trace.project !== project) return false
50
+ return true
51
+ })
52
+
53
+ const outcomes = traces.filter((t) => String(t.event_type || '').toLowerCase() === 'task_outcome')
54
+ const errors = traces.filter((t) => String(t.event_type || '').toLowerCase() === 'error')
55
+
56
+ // --- per-agent performance ---
57
+ const agentMap = new Map()
58
+ for (const outcome of outcomes) {
59
+ const agent = String(outcome.agent || 'unknown')
60
+ let entry = agentMap.get(agent)
61
+ if (!entry) {
62
+ entry = { agent, total: 0, success: 0, failure: 0, aborted: 0 }
63
+ agentMap.set(agent, entry)
64
+ }
65
+ entry.total += 1
66
+ const oc = String(outcome.outcome || 'none').toLowerCase()
67
+ if (oc === 'success') entry.success += 1
68
+ else if (oc === 'failure' || oc === 'error') entry.failure += 1
69
+ else entry.aborted += 1
70
+ }
71
+ const agents = [...agentMap.values()]
72
+ .map((entry) => ({
73
+ ...entry,
74
+ successRate: entry.total > 0 ? Math.round((entry.success / entry.total) * 100) : 0,
75
+ }))
76
+ .sort((a, b) => b.total - a.total)
77
+
78
+ // --- error hotspots ---
79
+ const errorByAgent = new Map()
80
+ const errorByTool = new Map()
81
+ for (const error of errors) {
82
+ const agent = String(error.agent || 'unknown')
83
+ errorByAgent.set(agent, (errorByAgent.get(agent) || 0) + 1)
84
+ const tool =
85
+ (error.payload && (error.payload.tool || error.payload.toolUseId || error.payload.status)) ||
86
+ 'unknown'
87
+ const key = agent + ':' + String(tool)
88
+ errorByTool.set(key, (errorByTool.get(key) || 0) + 1)
89
+ }
90
+ const errorHotspots = {
91
+ total: errors.length,
92
+ byAgent: [...errorByAgent.entries()]
93
+ .map(([agent, count]) => ({ agent, count }))
94
+ .sort((a, b) => b.count - a.count),
95
+ byTool: [...errorByTool.entries()]
96
+ .map(([key, count]) => ({ key, count }))
97
+ .sort((a, b) => b.count - a.count)
98
+ .slice(0, 10),
99
+ }
100
+
101
+ // --- project activity ---
102
+ const projectMap = new Map()
103
+ for (const outcome of outcomes) {
104
+ const name = String(outcome.project || 'unknown')
105
+ let entry = projectMap.get(name)
106
+ if (!entry) {
107
+ entry = { project: name, tasks: 0, success: 0, lastAt: null, recent: [] }
108
+ projectMap.set(name, entry)
109
+ }
110
+ entry.tasks += 1
111
+ if (String(outcome.outcome || '').toLowerCase() === 'success') entry.success += 1
112
+ const ts = Date.parse(outcome.timestamp || '')
113
+ if (Number.isFinite(ts) && (!entry.lastAt || ts > entry.lastAt)) entry.lastAt = ts
114
+ const summary = outcome.payload && outcome.payload.summary
115
+ if (summary && entry.recent.length < 3) {
116
+ entry.recent.push({
117
+ at: outcome.timestamp,
118
+ agent: outcome.agent,
119
+ outcome: outcome.outcome,
120
+ summary: String(summary).slice(0, 160),
121
+ })
122
+ }
123
+ }
124
+ const projects = [...projectMap.values()]
125
+ .map((entry) => ({ ...entry, lastAt: entry.lastAt ? new Date(entry.lastAt).toISOString() : null }))
126
+ .sort((a, b) => b.tasks - a.tasks)
127
+
128
+ // --- reuse evidence ---
129
+ const reuseRecords = readJsonl(path.join(home, 'experience_reuse.jsonl')).filter((record) => {
130
+ if (!record || typeof record !== 'object') return false
131
+ if (project && record.project !== project) return false
132
+ const ts = Date.parse(record.timestamp || '')
133
+ return Number.isFinite(ts) && ts >= cutoff
134
+ })
135
+ const reuse = {
136
+ verified: reuseRecords.filter(isVerifiedReuse).length,
137
+ pendingAutoClaims: reuseRecords.filter(
138
+ (record) => record.source === 'auto_claim' && record.confirmed !== true
139
+ ).length,
140
+ total: reuseRecords.length,
141
+ }
142
+
143
+ // --- rule-based suggestions (facts first, thresholds conservative) ---
144
+ const suggestions = []
145
+ const reliable = agents.filter((a) => a.total >= 5)
146
+ if (reliable.length >= 2) {
147
+ const best = reliable.reduce((a, b) => (b.successRate > a.successRate ? b : a))
148
+ const worst = reliable.reduce((a, b) => (b.successRate < a.successRate ? b : a))
149
+ if (best.successRate - worst.successRate >= 20) {
150
+ suggestions.push(
151
+ `路由信号: ${best.agent} 成功率 ${best.successRate}% vs ${worst.agent} ${worst.successRate}%(近 ${days} 天),同类任务优先路由给 ${best.agent}。`
152
+ )
153
+ }
154
+ }
155
+ if (errorHotspots.total >= 5 && errorHotspots.byAgent.length > 0) {
156
+ const top = errorHotspots.byAgent[0]
157
+ suggestions.push(
158
+ `错误热点: ${top.agent} 近 ${days} 天产生 ${top.count} 条 error trace${errorHotspots.byTool.length > 0 ? `(高发: ${errorHotspots.byTool[0].key} x${errorHotspots.byTool[0].count})` : ''},优先排查。`
159
+ )
160
+ }
161
+ if (reuse.pendingAutoClaims >= 10) {
162
+ suggestions.push(
163
+ `待确认: ${reuse.pendingAutoClaims} 条 auto-claim 经验复用未确认,运行 mio.experience.confirm 提升证据质量。`
164
+ )
165
+ }
166
+
167
+ const report = {
168
+ generatedAt: new Date(atTime).toISOString(),
169
+ periodDays: days,
170
+ project: project || 'all',
171
+ overview: {
172
+ tasks: outcomes.length,
173
+ byOutcome: outcomes.reduce((acc, t) => {
174
+ const oc = String(t.outcome || 'none').toLowerCase()
175
+ acc[oc] = (acc[oc] || 0) + 1
176
+ return acc
177
+ }, {}),
178
+ errorTraces: errors.length,
179
+ activeAgents: agents.length,
180
+ activeProjects: projects.length,
181
+ },
182
+ agents,
183
+ errorHotspots,
184
+ projects,
185
+ reuse,
186
+ suggestions,
187
+ }
188
+ report.markdown = renderMarkdown(report)
189
+ return report
190
+ }
191
+
192
+ function renderMarkdown(report) {
193
+ const lines = []
194
+ const from = new Date(Date.parse(report.generatedAt) - report.periodDays * 86400000)
195
+ .toISOString()
196
+ .slice(0, 10)
197
+ const to = report.generatedAt.slice(0, 10)
198
+ lines.push(`# Mio Digest (${from} ~ ${to}, project=${report.project})`)
199
+ lines.push('')
200
+ const ov = report.overview
201
+ const rate = ov.tasks > 0 ? Math.round(((ov.byOutcome.success || 0) / ov.tasks) * 100) : 0
202
+ lines.push(
203
+ `- 任务 ${ov.tasks} 条(success ${ov.byOutcome.success || 0} / failure ${ov.byOutcome.failure || 0} / aborted ${ov.byOutcome.aborted || 0},成功率 ${rate}%),error trace ${ov.errorTraces} 条`
204
+ )
205
+ lines.push(`- 活跃 agent ${ov.activeAgents} 个,活跃项目 ${ov.activeProjects} 个`)
206
+ lines.push('')
207
+
208
+ if (report.agents.length > 0) {
209
+ lines.push('## Agent 表现')
210
+ lines.push('')
211
+ lines.push('| agent | 任务 | 成功 | 失败 | 中止 | 成功率 |')
212
+ lines.push('|---|---|---|---|---|---|')
213
+ for (const a of report.agents) {
214
+ lines.push(`| ${a.agent} | ${a.total} | ${a.success} | ${a.failure} | ${a.aborted} | ${a.successRate}% |`)
215
+ }
216
+ lines.push('')
217
+ }
218
+
219
+ if (report.projects.length > 0) {
220
+ lines.push('## 项目动态')
221
+ lines.push('')
222
+ for (const p of report.projects) {
223
+ const lastAt = p.lastAt ? p.lastAt.slice(0, 16).replace('T', ' ') : '-'
224
+ lines.push(`### ${p.project}(${p.tasks} 任务,最近 ${lastAt})`)
225
+ for (const task of p.recent) {
226
+ const at = task.at ? task.at.slice(5, 16).replace('T', ' ') : ''
227
+ lines.push(`- [${task.outcome}] (${at} ${task.agent}) ${task.summary}`)
228
+ }
229
+ lines.push('')
230
+ }
231
+ }
232
+
233
+ if (report.errorHotspots.total > 0) {
234
+ lines.push('## 错误热点')
235
+ lines.push('')
236
+ for (const item of report.errorHotspots.byAgent) {
237
+ lines.push(`- ${item.agent}: ${item.count} 条`)
238
+ }
239
+ const tools = report.errorHotspots.byTool.slice(0, 5)
240
+ if (tools.length > 0) {
241
+ lines.push(`- 高发位置: ${tools.map((t) => `${t.key} x${t.count}`).join(', ')}`)
242
+ }
243
+ lines.push('')
244
+ }
245
+
246
+ lines.push('## 经验复用')
247
+ lines.push('')
248
+ lines.push(`- verified ${report.reuse.verified} / pending auto-claim ${report.reuse.pendingAutoClaims} / total ${report.reuse.total}`)
249
+ lines.push('')
250
+
251
+ if (report.suggestions.length > 0) {
252
+ lines.push('## 可行动建议')
253
+ lines.push('')
254
+ for (const suggestion of report.suggestions) {
255
+ lines.push(`- ${suggestion}`)
256
+ }
257
+ lines.push('')
258
+ }
259
+
260
+ return lines.join('\n')
261
+ }
262
+
263
+ return { generate, renderMarkdown }
264
+ }
265
+
266
+ module.exports = { createDigest }
@@ -5,6 +5,7 @@ const adapters = {
5
5
  opencode: require('../adapters/opencode.js'),
6
6
  workbuddy: require('../adapters/workbuddy.js'),
7
7
  hermes: require('../adapters/hermes.js'),
8
+ claude: require('../adapters/claude-code.js'),
8
9
  }
9
10
 
10
11
  const HOST_CAPABILITIES = [
@@ -24,6 +25,10 @@ const HOST_CAPABILITIES = [
24
25
  name: 'hermes',
25
26
  capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'observer-transcript', 'policy-check', 'experience-reuse', 'runtime-status'],
26
27
  },
28
+ {
29
+ name: 'claude',
30
+ capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'observer-transcript', 'policy-check', 'experience-reuse', 'runtime-status'],
31
+ },
27
32
  ]
28
33
 
29
34
  function safeInstalled(adapter) {